文档结构  
翻译进度:已翻译     翻译赏金:0 元 (?)    ¥ 我要打赏

这是Go的第三部分编程介绍。 要开始运行,请先从第一部分开始。

第二部分中,我们学习了如何使用变量并更新我们的程序来解析命令行标志。 这第三篇文章是我们终于找到有趣的部分:生成GIF! 我们先来讨论一些更高级(非标量)的变量类型。

非标量变量

数组

数组是一个包含相同类型的多个值的变量。 它具有固定的大小,并且数组中的每个元素被赋予一个从零开始的索引。 例如,以下代码声明一个5个整数的数组:

第 1 段(可获 1.36 积分)
var things [5]int
// indexes will be 0,1,2,3,4

Go自动将每个元素初始化为“零值”,所以things是一个值为5个零的列表。 每个元素都可以通过其索引访问来获取和设置:

fmt.Println(things[0]) // prints "0", the value of the first element
things[0] = 20 // assigns the value 20 to the first element
fmt.Println(things[0]) // prints "20"

要一次设置所有值,请在类型后面的大括号内写入值,并用逗号分隔值:

things = [5]int{2,4,6,8,10}

还有一个内置函数len()来获取数组(或切片)的长度:

第 2 段(可获 0.76 积分)
fmt.Println(len(things)) // prints 5

问题:如果你需要添加第六个整数到我们的things数组怎么办? 对不起,你在声明时给出数组的大小是它大小永远的限制。 你增加数组大小的唯一选择是创建一个新的更大的数组,然后将旧数组中的值复制到其中。

切片

数组是任何语言进行编程的基础,但在Go中很少与他们直接交互。 你可能更喜欢使用方便的“切片”。 Go中的一个切片基本上是一个数组的包装,使数据列表更方便使用。

第 3 段(可获 1.33 积分)

如上所述,数组永远不会改变大小。但是, 切片是一个优雅的小技巧。 切片只显示一部分底层数组(因此名称为“slice”),因此它似乎是一个较短的数组。 每次向切片添加元素时,Go会检查切片的底层数组中是否有足够的空间。 如果有,你的新值将放入数组的下一个未使用的位置中; 如果没有,则创建一个更大的数组来替换第一个数组并将其放入其中。 无论哪种方式,切片会被延长,以便你“看到”数组更多的内容。 就是这样的。

第 4 段(可获 1.39 积分)

如果不这样做,别担心。 当您使用它们时,切片会更有意义。

切片用方括号(如数组)声明,但没有大小。

var someSlice []int

警告:与数组不同,切片在声明时间内无法使用。 切片类型的变量不会自动包含实际切片。 你必须给它分配一个。

创建切片变量的实际切片有两种方法:使用花括号(与数组一起)或make()。

var someSlice []int
// Implicitly create a slice of length 3 with the given values.
someSlice = []int{1,2,3}
// Explicitly create a slice of length 3 with an underlying array of length 5.
// The last two elements in the underlying array will be hidden until needed.
someSlice = make([]int, 3, 5)
第 5 段(可获 1.06 积分)

与数组一样,您可以通过索引访问切片的元素。 关于切片的很酷的事情是,您可以使用append()函数添加其他元素。 append()将切片作为第一个参数,并将要添加的值作为第二个参数。

var slice []int = []int{1,2,3}
fmt.Println(len(slice)) // prints "3"
slice = append(slice, 25)
fmt.Println(len(slice)) // prints "4"
fmt.Println(slice[3]) // prints "25"

注意我们如何写slice = append(slice,25)。 append()函数不会修改该片段,而是返回一个新的片断,因此请始终记住重新分配的结果。

第 6 段(可获 0.86 积分)

映射

数组和切片对于某些类型的列表而言非常棒,但对其他类型的不是。 假设你想记录你朋友的生日。 你可以写一个包含生日的数组,但是你不知道哪个生日属于哪个朋友。 对于这些情况,Go给我们map。

映射很像数组/切片,因为它们具有多个值,但不是通过有序数字键进行索引,您可以选择所需的任何键。 我们的例子中,你可以使用你朋友的名字作为键。

要声明一个映射,在“map”后的方括号中写入键的类型,然后是键对应的值类型。 与切片一样,你还必须在使用之前创建并将给该变量指定实际映射。 这是我们的例子:

第 7 段(可获 1.58 积分)
// Declare and assign a map with string keys and string values.
// As with slices, we could use make() instead of {}.
var birthdays map[string]string = map[string]string{}
// Add some birthdays to our map.
birthdays["joe"] = "9/30/1993"
birthdays["michele"] = "4/1/1998"

结构

Go中最灵活的变量类型是struct。 实际上,在定义自己的类型时,最常使用类型也是结构 - 它们太灵活了。 你现在可能已经注意到,我们定义的每个变量,甚至是切片和映射,只能容纳一种类型的变量。 即使它可以拥有很多的值,即使你可以按想要的命名他们,他们仍然必须是相同的类型。 结构是一种特殊的类型,可以用任何你想要的名字来保存任何你想要的值。

第 8 段(可获 1.21 积分)

我们不会在结构这里停留很久,因为我们只在程序中与他们进行了一点交互。 但是看一下使用结构的这个例子:

type Person struct {
    Name string
    Age int
    Height float32
}

var somebody Person = Person{Name: "Bob", Age: 55, Height: 5.9}
fmt.Println(somebody.Name) // Prints "Bob"
somebody.Name = "Alyssa"
fmt.Println(somebody.Name) // Prints "Alyssa"

注意我们如何访问我们的结构变量somebody的属性:变量名,点,属性名。

继续我们的计划

现在,您知道Go中的所有主要类型,我们可以继续做我们的GIF生成器。

第 9 段(可获 0.76 积分)

获取文件

如果你记得,我们的GIF生成器知道从中获取GIF图像的路径,所以接下来要做的就是获取这些图像。

导入io / ioutil 和os包。 第一个包含一大堆方便的输入/输出助手; 第二个给我们一些平台无关的操作系统工具

import (
    "flag"
    "fmt"
    "io/ioutil"
    "os"
)

ioutil带有一个ReadDir()函数 - 你猜到它的作用了 - 读取一个目录的内容。 在main()函数的底部,我们来看看我们要加载的图像的路径:

第 10 段(可获 1.06 积分)
func main() {
    // ...

    // fmt.Println("This will be a GIF generator!")
    var files []os.FileInfo
    var err error
    files, err = ioutil.ReadDir(path)
    if err != nil {
        fmt.Println(err)
        return
    }
}

首先,我们声明我们的变量,最重要的是files,它是一个ofos.FileInfo结构体的切片(而不是数组)。 然后我们将ioutil.ReadDir()的结果赋值给我们的变量,并遇到两个新概念:多个返回值和错误处理。

多个返回值:注意我们如何从ioutil.ReadDir()中同时获取 files 和 err 了吗? Go中的函数可能会返回由逗号分隔的多个值,即return thing1,thing2。 当一个函数执行此操作时,调用者必须接收多个值。 如果你真的想忽略返回的值,请在正确的位置放置_(下划线)而不是变量名称。 例如,我们可以通过这样写 files,_ = ioutil.ReadDir(path)来忽略错误。 这样是很不好的。 不要忽略错误。

第 11 段(可获 1.29 积分)

错误处理:通常的做法是使函数的最后一个返回值为错误类型的变量。 如果没有发生错误,该变量将为空,这基本上意味着它没有任何东西。 我们已经做了最简单的错误处理形式:如果它不是空,打印错误然后退出该函数。

重构和短变量声明

回头看最后一个例子。 注意它有多么臃肿了吗? 随着你的程序变得越来越大,你应该在它失控之前经常花时间去优化它。 这种优化通常被称为“重构”。 让我们通过使我们的变量声明更短来做我们的第一次重构。

第 12 段(可获 1.44 积分)

到目前为止,我们已经明确声明了我们的变量的类型,并用值单独地初始化了它们,这意味着写了两次类型。 例如:

var someSlice []int = []int{1,2,3}

这很乏味。 Go给我们一个特殊的操作符 :=, 它使用你提供的值的类型隐式地声明变量。 前面的例子变成:

someSlice := []int{1,2,3}

这很清楚,我们正在生成一个整形的切片,但是我们只需要写一次。 这是一场胜利。

简短的声明运算符:= 也声明变量,因此就像var对于给定的变量名称只能使用一次。 给已存在的变量使用:= 将抛出错误。

第 13 段(可获 1.43 积分)

如果我们使用:=,我们不必显式声明files 和err。 替代的是,他们将通过ioutil.ReadDir()返回值声明,无论返回值是什么类型,像这样:

func main() {
    // ...

    // fmt.Println("This will be a GIF generator!")
    files, err := ioutil.ReadDir(path)
    if err != nil {
        fmt.Println(err)
        return
    }
}

我们的第一个重构已经删除了两行不必要的代码,使我们的程序更整洁。

for循环

ioutil.ReadDir()已经给了我们一个文件的切片。 我们需要编写一个重复运行的代码块,每个文件运行一次。 我们称之为“循环”。 Go只有一种类型的循环,但它可以以多种方式使用。 我会教你们结合range函数,这可以认为是“对于这个范围内的每一个事情,都做这个”。

第 14 段(可获 1.34 积分)

要循环我们的文件,打印每个文件的名称,我们这样写:

for _, info := range files {
    fmt.Println(info.Name())
}

在数组或切片上使用range函数返回每个元素的索引和值。 因为我们不需要索引,所以我们放一个下划线而不是一个变量名。 我们已经调用了info的值,因为它是一个os.FileInfo对象/结构体。

花括号中的代码会为files中的每个元素运行一次,每次在info中获取一个新值。

创建GIF

现在你快要完成了,所以我们来使用一些图像处理工具,把所有的东西放在一起,并建立一个GIF。

第 15 段(可获 1.25 积分)

导入包 imageimage/color/paletteimage/drawimage/gif, 和 image/jpeg (但请参阅说明):

import (
    "flag"
    "fmt"
    "image"
    "image/color/palette"
    "image/draw"
    "image/gif"
    "io/ioutil"
    "os"

    _ "image/jpeg"
)

最后一个应该脱颖而出。 我们不直接需要image / jpeg,但是导入它意味着image.Decode()将能够解码JPEG。 将下划线放在包的前面,让我们在即使代码不使用它,也可以为了副作用将其导入。

接下来我们需要创建一个新的gif.GIF结构体。 这将保存我们用于编码最终GIF所需的所有信息。 把它放在你的for循环前面:

第 16 段(可获 0.96 积分)
anim := gif.GIF{}

为了简洁起见我们再次使用简短的声明:=,并实例化结构,但不放入任何东西--因此大括号是空的。

在你的for循环中,我们需要打开每个文件:

for _, info := range files {
    f, err := os.Open(path + "/" + info.Name())
    if err != nil {
        fmt.Printf("Could not open file %s. Error: %s\n", info.Name(), err)
        return
    }
    defer f.Close()
}

我们一步一步进行:

1.尝试使用os.Open()。os.Open()返回指向os.File对象的指针和错误(如果有的话)并打开文件。

2.在使用打开的文件之前检查错误。如果出现错误,我们使用fmt.Printf()打印一个自定义消息,该消息采用格式化字符串,其中包含占位符,后面跟着替换占位符的值。 %s表示“将值视为字符串”。第一个%s将被替换为info.Name()的结果;第二个%s将被err的值代替。

3.完成后,确保文件关闭。保持文件打开会占用资源,所以我们需要关闭它。但下面的代码可能需要几个不同的路径,这可能意味着必须在多个地方关闭它。 Go让我们使用defer 函数声明这种情况。 作为函数退出之前的最后一件事情,defer会运行一个函数,允许我们不管什么情况发生,在main()的结尾关闭文件。

第 17 段(可获 2.6 积分)

注意:我们在 fmt.Printf()消息的末尾放置了一个新的行字符 \n。 顾名思义,fmt.Println()打印出你的字符串并跟一个换行符,为了它不会与下一个 fmt.Printf()中的东西混在一起,所以我们自己添加一个换行符。

接下来,我们需要将我们打开的图像使用调色板进行转换,然后将它们添加到我们的gif.GIF对象中。 在你的for循环内部 defer 之后,跳过一行并添加以下内容:

palleted := image.NewPaletted(img.Bounds(), palette.Plan9)
draw.FloydSteinberg.Draw(palleted, img.Bounds(), img, image.ZP)

anim.Image = append(anim.Image, palleted)
anim.Delay = append(anim.Delay, delay*100)
第 18 段(可获 0.99 积分)

在第一行,我们创建一个新的与img相同大小的空白图像,把它叫做 paletted。 接下来,我们将img的内容绘制在 paletted 上,有效地将图像从一个复制到另一个。

第三行把paletted 添加到anim.Image 切片,这是放置在GIF中的帧列表。 最后,我们将这些帧的延迟添加到admin.Delay切片。 (anim.Delay是在几百秒,所以我们多延迟100)

最后,我们用三行代码将输出一个GIF(将它们放在main()的结尾,循环之外):

f, _ := os.Create(output)
defer f.Close()
gif.EncodeAll(f, &anim)
第 19 段(可获 1.19 积分)

记住output应包含用户给定的输出文件名,这一切都应该行的通的。 gif.EncodeAll()接受anim的内容,将其编码为GIF,并将结果放在f , f指向我们的文件。

我们完成了

你完成的程序应该如下所示:

package main

import (
    "flag"
    "fmt"
    "image"
    "image/color/palette"
    "image/draw"
    "image/gif"
    "io/ioutil"
    "os"

    _ "image/jpeg"
)

var path, output string
var delay int

func main() {
    flag.StringVar(&path, "p", "", "path to the folder containing images")
    flag.StringVar(&output, "o", "output.gif", "the name of the generated GIF")
    flag.IntVar(&delay, "d", 5, "delay between frames in seconds")
    flag.Parse()

    if path == "" {
        fmt.Println("A path is required")
        flag.PrintDefaults()
        return
    }

    if delay < 1 || delay > 10 {
        fmt.Println("delay must be between 1 and 10 inclusively")
        return
    }

    // fmt.Println("This will be a GIF generator!")
    files, err := ioutil.ReadDir(path)
    if err != nil {
        fmt.Println(err)
        return
    }

    anim := gif.GIF{}
    for _, info := range files {
        f, err := os.Open(path + "/" + info.Name())
        if err != nil {
            fmt.Printf("Could not open file %s. Error: %s\n", info.Name(), err)
            return
        }
        defer f.Close()
        img, _, _ := image.Decode(f)

        // Image has to be palleted before going into the GIF
        paletted := image.NewPaletted(img.Bounds(), palette.Plan9)
        draw.FloydSteinberg.Draw(paletted, img.Bounds(), img, image.ZP)

        anim.Image = append(anim.Image, paletted)
        anim.Delay = append(anim.Delay, delay*100)
    }

    f, _ := os.Create(output)
    defer f.Close()
    gif.EncodeAll(f, &anim)
}

运行go install,将一些图像放在一个文件夹中,生成一个GIF吧。

第 20 段(可获 0.74 积分)

下一步和进一步阅读

Go by Example 覆盖了许多比本教程更容易理解的例子,并带有简单的解释,而官方的Tour of Go,会带你一步一个脚印学到所有Go的知识点。Effective Go更适合有经验的开发人员,但仍然是必读的。你可以从现在开始就得到它们,并且随着你的编程成熟,继续前进。

同时,通过改进你刚刚写的内容来锻炼你的技能:*调用 image.Decode()不检查返回值,所以使用 img 的任何方法都可能会中断。我们在使用os.Create()保存输出时也是一样。您可以通过处理任何可能的错误使代码更强大吗? *我们可以读取JPEG而不能读取PNG。你可以导入image/png包来修复这个问题吗? *我们对 files 的循环尝试打开所有文件,甚至包括非图像文件。在继续之前,你可以在if子句中检查文件的扩展名吗? (提示:strings包的HasSuffix()函数可能有帮助。)

一如以往,请随时在评论中寻求帮助。编码快乐!

第 21 段(可获 2.14 积分)

文章评论