命名

如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如 fmt 包的 Printf 函数就是导出的,可以在 fmt 包外部访问。

声明

包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)。

变量

在包级别声明的变量会在 main 入口函数执行前完成初始化(§2.6.2),局部变量将在声明语句被执行到的时候完成初始化。

在函数内部,有一种称为简短变量声明语句的形式可用于声明和初始化局部变量。它以“名字 := 表达式”形式声明变量,变量的类型根据表达式来自动推导。

T := 0.0
S := "hello, world"

和 var 形式声明语句一样,简短变量声明语句也可以用来声明和初始化一组变量:

I, j := 0, 1

简短变量声明左边的变量可能并不是全部都是刚刚声明的。如果有一些已经在相同的词法域声明过了(§2.7),那么简短变量声明语句对这些已经声明过的变量就只有赋值行为了

简短变量声明语句中必须至少要声明一个新的变量,下面的代码将不能编译通过:

F, err := os.Open (infile)
// ...
F, err := os.Create (outfile) // compile error: no new variables

解决的方法是第二个简短变量声明语句改用普通的多重赋值语句。

指针

如果用“var x int”声明语句声明一个 x 变量,那么&x 表达式(取 x 变量的内存地址)将产生一个指向该整数变量的指针,指针对应的数据类型是 *int,指针被称之为“指向 int 类型的指针”。如果指针名字为 p,那么可以说“p 指针指向变量 x”,或者说“p 指针保存了 x 变量的内存地址”。同时 *p 表达式对应 p 指针指向的变量的值。一般 *p 表达式读取指针指向的变量的值,这里为 int 类型的值,同时因为 *p 对应一个变量,所以该表达式也可以出现在赋值语句的左边,表示更新指针所指向的变量的值。

New 函数

另一个创建变量的方法是调用内建的 new 函数。表达式 new (T)将创建一个 T 类型的匿名变量,初始化为 T 类型的零值,然后返回变量地址,返回的指针类型为 *T

p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

用 new 创建变量和普通变量声明语句方式创建变量没有什么区别,除了不需要声明一个临时变量的名字外,我们还可以在表达式中使用 new (T)。换言之,new 函数类似是一种语法糖,而不是一个新的基础概念。

==注:==new 函数使用通常相对比较少,因为对于结构体来说,直接用字面量语法创建新变量的方法会更灵活(§4.4.1)

变量的声明周期

对于在包一级声明的变量来说,它们的生命周期和整个程序的运行周期是一致的。而相比之下,局部变量的生命周期则是动态的:每次从创建一个新变量的声明语句开始,直到该变量不再被引用为止,然后变量的存储空间可能被回收。

==可达性分析:==那么 Go 语言的自动垃圾收集器是如何知道一个变量是何时可以被回收的呢?这里我们可以避开完整的技术细节,基本的实现思路是,从每个包级的变量和每个当前运行函数的每一个局部变量开始,通过指针或引用的访问路径遍历,是否可以找到该变量。如果不存在这样的访问路径,那么说明该变量是不可达的,也就是说它是否存在并不会影响程序后续的计算结果。

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用 var 还是 new 声明变量的方式决定的。

var global *int

func f() {
    var x int
    x = 1
    global = &x
}

func g() {
    y := new(int)
    *y = 1
}

f 函数里的 x 变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的 global 变量找到,虽然它是在函数内部定义的;用 Go 语言的术语说,这个 x 局部变量从函数 f 中逃逸了。相反,当 g 函数返回时,变量 *y 将是不可达的,也就是说可以马上被回收的。因此,*y 并没有从函数 g 中逃逸,编译器可以选择在栈上分配 *y 的存储空间(译注:也可以选择在堆上分配,然后由 Go 语言的 GC 回收这个变量的内存空间),虽然这里用的是 new 方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。

Go 语言的自动垃圾收集器对编写正确的代码是一个巨大的帮助,但也并不是说你完全不用考虑内存了。你虽然不需要显式地分配和释放内存,但是要编写高效的程序你依然需要了解变量的生命周期。例如,如果将指向短生命周期对象的指针保存到具有长生命周期的对象中,特别是保存到全局变量时,会阻止对短生命周期对象的垃圾回收(从而可能影响程序的性能)。

赋值

元组赋值

元组赋值是另一种形式的赋值语句,它允许同时更新多个变量的值。在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。这对于处理有些同时出现在元组赋值语句左右两边的变量很有帮助,例如我们可以这样交换两个变量的值:

x, y = y, x

a[i], a[j] = a[j], a[i]

但如果表达式太复杂的话,应该尽量避免过度使用元组赋值;因为每个变量单独赋值语句的写法可读性会更好。

有些表达式会产生多个值,比如调用一个有多个返回值的函数。当这样一个函数调用出现在元组赋值右边的表达式中时(译注:右边不能再有其它表达式),左边变量的数目必须和右边一致。

f, err = os.Open("foo.txt") // function call returns two values

类型

类型声明语句一般出现在包一级,因此如果新创建的类型名字的首字符大写,则在包外部也可以使用。

import "fmt"

type Celsius float64    // 摄氏温度
type Fahrenheit float64 // 华氏温度

const (
    AbsoluteZeroC Celsius = -273.15 // 绝对零度
    FreezingC     Celsius = 0       // 结冰点温度
    BoilingC      Celsius = 100     // 沸水温度
)

func CToF(c Celsius) Fahrenheit { return Fahrenheit(c*9/5 + 32) }

func FToC(f Fahrenheit) Celsius { return Celsius((f - 32) * 5 / 9) }

我们在这个包声明了两种类型:Celsius 和 Fahrenheit 分别对应不同的温度单位。它们虽然有着相同的底层类型 float 64但是它们是不同的数据类型,因此它们不可以被相互比较或混在一个表达式运算刻意区分类型,可以避免一些像无意中使用不同单位的温度混合计算导致的错误;因此需要一个类似 Celsius (t)或 Fahrenheit (t)形式的显式转型操作才能将 float 64 转为对应的类型。Celsius (t)和 Fahrenheit (t)是类型转换操作,它们并不是函数调用。类型转换不会改变值本身,但是会使它们的语义发生变化。另一方面,CToF 和 FToC 两个函数则是对不同温度单位下的温度进行换算,它们会返回不同的值。

对于每一个类型 T,都有一个对应的类型转换操作 T (x),用于将 x 转为 T 类型(译注:如果 T 是指针类型,可能会需要用小括弧包装 T,比如 (*int)(0))。只有当两个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。如果 x 是可以赋值给 T 类型的值,那么 x 必然也可以被转为 T 类型,但是一般没有这个必要。

比较运算符 ==< 也可以用来比较一个命名类型的变量和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较。但是如果两个值有着不同的类型,则不能直接进行比较

var c Celsius
var f Fahrenheit
fmt.Println(c == 0)          // "true"
fmt.Println(f >= 0)          // "true"
fmt.Println(c == f)          // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

包和文件

在每个源文件的===包声明前==紧跟着的注释是包注释(§10.7.4)。通常,包注释的第一句应该先是包的功能概要说明。一个包通常只有一个源文件有包注释(译注:如果有多个包注释,目前的文档工具会根据源文件名的先后顺序将它们链接为一个包注释)。如果包注释很大,通常会放到一个独立的 doc. Go 文件中。

// Cf converts its numeric argument to Celsius and Fahrenheit.
package main

import (
    "fmt"
    "os"
    "strconv"

    "gopl.io/ch2/tempconv"
)

func main() {
    for _, arg := range os.Args[1:] {
        t, err := strconv.ParseFloat(arg, 64)
        if err != nil {
            fmt.Fprintf(os.Stderr, "cf: %v\n", err)
            os.Exit(1)
        }
        f := tempconv.Fahrenheit(t)
        c := tempconv.Celsius(t)
        fmt.Printf("%s = %s, %s = %s\n",
            f, tempconv.FToC(f), c, tempconv.CToF(c))
    }
}

导入语句将导入的包绑定到一个短小的名字,然后通过该短小的名字就可以引用包中导出的全部内容。上面的导入声明将允许我们以 tempconv. CToF 的形式来访问 gopl. Io/ch 2/tempconv 包中的内容。在默认情况下,导入的包绑定到 tempconv 名字(译注:指包声明语句指定的名字),但是我们也可以绑定到另一个名称,以避免名字冲突(§10.4)。

包的初始化

var a = b + c // a 第三个初始化, 为 3
var b = f()   // b 第二个初始化, 为 2, 通过调用 f (依赖c)
var c = 1     // c 第一个初始化, 为 1

func f() int { return c + 1 }

如果包中含有多个. Go 源文件,它们将按照发给编译器的顺序进行初始化,Go 语言的构建工具首先会将. Go 文件根据文件名排序,然后依次调用编译器编译。

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例如某些表格数据初始化并不是一个简单的赋值过程。在这种情况下,我们可以用一个特殊的 init 初始化函数来简化初始化工作。每个文件都可以包含多个 init 初始化函数

func init() { /* ... */ }

这样的 init 初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的 init 初始化函数,在程序开始执行时按照它们声明的顺序被自动调用

作用域

不要将作用域和生命周期混为一谈。声明语句的作用域对应的是一个源代码的文本区域;它是一个编译时的属性。一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序的其他部分引用;是一个运行时的概念。

句法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧包裹的内容一样。句法块内部声明的名字是无法被外部块访问的。这个块决定了内部声明的名字的作用域范围。我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块。对全局的源代码来说,存在一个整体的词法块,称为全局词法块;对于每个包;每个 for、if 和 switch 语句,也都有对应词法块;每个 switch 或 select 的分支也有独立的词法块;当然也包括显式书写的词法块(花括弧包含的语句)。

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。如果查找失败,则报告“未声明的名字”这样的错误。如果该名字在内部和外部的块分别声明过,则内部块的声明首先被找到。在这种情况下,内部声明屏蔽了外部同名的声明,让外部的声明的名字无法被访问。

var cwd string

func init() {
    cwd, err := os.Getwd() // compile error: unused: cwd
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

虽然 cwd 在外部已经声明过,但是 := 语句还是将 cwd 和 err 重新声明为新的局部变量因为内部声明的 cwd 将屏蔽外部的声明,因此上面的代码并不会正确更新包级声明的 cwd 变量

由于当前的编译器会检测到局部声明的 cwd 并没有使用,然后报告这可能是一个错误,但是这种检测并不可靠。因为一些小的代码变更,例如增加一个局部 cwd 的打印语句,就可能导致这种检测失效。

var cwd string

func init() {
    cwd, err := os.Getwd() // NOTE: wrong!
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

全局的 cwd 变量依然是没有被正确初始化的,而且看似正常的日志输出更是让这个 BUG 更加隐晦。

有许多方式可以避免出现类似潜在的问题。最直接的方法是通过单独声明 err 变量,来避免使用 := 的简短声明方式

var cwd string

func init() {
    var err error
    cwd, err = os.Getwd()
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
}

函数

Deferred 函数

你只需要在调用普通函数或方法前加上关键字 defer,就完成了 defer 所需要的语法。当执行到该条语句时,函数和参数表达式得到计算,但直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

Defer 语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。通过 defer 机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。释放资源的 defer 应该直接跟在请求资源的语句后。

Goroutine

当一个 goroutine 尝试在一个 channel 上做 send 或者 receive 操作时,这个 goroutine 会阻塞在调用处,直到另一个 goroutine 从这个 channel 里接收或者写入值,这样两个 goroutine 才会继续执行 channel 操作之后的逻辑。在这个例子中,每一个 fetch 函数在执行时都会往 channel 里发送一个值(ch <- expression),主函数负责接收这些值(<-ch)。这个程序中我们用 main 函数来接收所有 fetch 函数传回的字符串,可以避免在 goroutine 异步执行还没有完成时 main 函数提前退出。

构建可执行程序

Win程序包构建

set GOOS=windows
set GOARCH=amd64
go build -o hello.exe main.go

Linux程序包构建

set GOOS=linux
set GOARCH=amd64
go build -o updateBlogWebhook main.go

待练习的程序

Flag

package main

import (
    "flag"
    "fmt"
    "strings"
)

var n = flag.Bool("n", false, "omit trailing newline")
var sep = flag.String("s", " ", "separator")

func main() {
    flag.Parse()
    fmt.Print(strings.Join(flag.Args(), *sep))
    if !*n {
        fmt.Println()
    }
}

调用 flag. Bool 函数会创建一个新的对应布尔型标志参数的变量。它有三个属性:第一个是命令行标志参数的名字“n”,然后是该标志参数的默认值(这里是 false),最后是该标志参数对应的描述信息。如果用户在命令行输入了一个无效的标志参数,或者输入 -h-help 参数,那么将打印所有标志参数的名字、默认值和描述信息。类似的,调用 flag. String 函数将创建一个对应字符串类型的标志参数变量,同样包含命令行标志参数对应的参数名、默认值、和描述信息。程序中的 sepn 变量分别是指向对应命令行标志参数变量的指针,因此必须用 *sep*n 形式的指针语法间接引用它们。

当程序运行时,必须在使用标志参数对应的变量之前先调用 flag. Parse 函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用 flag.Args ()函数来访问,返回值对应一个字符串类型的 slice。如果在 flag. Parse 函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用 os.Exit (2)终止程序。

当程序运行时,必须在使用标志参数对应的变量之前先调用 flag. Parse 函数,用于更新每个标志参数对应变量的值(之前是默认值)。对于非标志参数的普通命令行参数可以通过调用 flag.Args ()函数来访问,返回值对应一个字符串类型的 slice。如果在 flag. Parse 函数解析命令行参数时遇到错误,默认将打印相关的提示信息,然后调用 os.Exit (2)终止程序。

$ go build gopl.io/ch2/echo4
$ ./echo4 a bc def
a bc def
$ ./echo4 -s / a bc def
a/bc/def
$ ./echo4 -n a bc def
a bc def$
$ ./echo4 -help
Usage of ./echo4:
  -n    omit trailing newline
  -s string
        separator (default " ")