2.1 命名

命名规则

以字母或下划线开头,后面可以跟任意数量的数字,字母,下划线

关键字有25个

1
2
3
4
5
break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

预定义的名字

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

作用域

  • 函数内部定义的变量,作用域仅为当前函数内部
  • 在函数外部定义的变量,可以在当前包的所有文件访问 (要设置run on package)
  • 函数外部定义的、大写字母开头的变量(函数),那么它将是导出的(可以被外部的包访问)

2.2 声明

四种声明语句:

  • var
  • const
  • type
  • func

函数的声明

  • 一个函数的声明由一个函数名字、参数列表(由函数的调用者提供参数变量的具体值)、一个可选的返回值列表和包含函数定义的函数体组成。
  • 如果函数没有返回值,那么返回值列表是省略的
1
2
3
func fn(f float64) float64{
    return 3.14
}

2.3 变量

变量声明语法:

1
var 变量名 类型 = 表达式
  • 其中类型和表达式可以省略其中一个,由自动推导完成
  • 如果初始化表达式被省略,那么将用零值初始化该变量。

零值定义

  • 数值类型变量对应的零值是0
  • 布尔类型变量对应的零值是false
  • 字符串类型对应的零值是空字符串
  • 接口或引用类型(包括slice、指针、map、chan和函数)变量对应的零值是nil
  • 数组或结构体等聚合类型对应的零值是每个元素或字段都是对应该类型的零值

零值初始化机制可以确保每个声明的变量总是有一个良好定义的值,因此在Go语言中不存在未初始化的变量

一些常见的变量初始化

1
2
3
var i, j, k int                 // int, int, int
var b, f, s = true, 2.3, "four" // bool, float64, string
var f, err = os.Open(name) // os.Open returns a file and an error

简短变量声明

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

1
2
i := 100
freq = rand.Float64() * 3.0

注意事项:

  • 简短变量声明语句中必须至少要声明一个新的变量 (见下面第一个程序)
  • :=是声明,而不是赋值
  • 简短变量声明语句只有对已经在同级词法域声明过的变量才和赋值操作语句等价,如果变量是在外部词法域声明的,那么简短变量声明语句将会在当前词法域重新声明一个新的变量
1
2
f, err := os.Open(infile)
f, err := os.Create(outfile) // compile error: no new variables

指针

在Go语言中,返回函数中局部变量的地址也是安全的。

1
2
3
4
5
6
var p = f()

func f() *int {
    v := 1
    return &v
}

但是需要注意,每次调用的返回值都不同

1
fmt.Println(f() == f()) // "false"

flag包中

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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()
    }
}

其中n和sep都是指针。运行命令

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ 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 " ")

new函数

new(T)将创建一个类型为T的匿名变量,初始化为T的零值,并返回其指针

1
2
p := new(int)
*p = 2

如果两个类型都是空的,也就是说类型的大小是0,例如struct{}和[0]int,有可能有相同的地址(依赖具体的语言实现)(译注:请谨慎使用大小为0的类型,因为如果类型的大小为0的话,可能导致Go语言的自动垃圾回收器有不同的行为,具体请查看runtime.SetFinalizer函数相关文档)。

变量的生命周期

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

编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,这个选择并不是由用var还是new声明变量的方式决定的。(牵扯出逃逸问题)

2.4 赋值

不支持 x = i++

元组赋值

先计算右边所有的表达式,然后统一赋值给左边

1
2
x, y = y, x
a[i], a[j] = a[j], a[i]

2.5 类型

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构

1
type 类型名字 底层类型

新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的

2.6 包和文件

通常一个包所在目录路径的后缀是包的导入路径。例如包gopl.io/ch1/helloworld对应的目录路径是$GOPATH/src/gopl.io/ch1/helloworld

包的初始化

  • 涉及变量的初始化顺序
  • 可以用一个特殊的init初始化函数来简化初始化工作。每个文件都可以包含多个init初始化函数。
  • init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。在每个文件中的init初始化函数,在程序开始执行时按照它们声明的顺序被自动调用。
  • 可以使用匿名函数进行初始化
1
2
3
4
5
6
var pc [256]byte = func() (pc [256]byte) {
    for i := range pc {
        pc[i] = pc[i/2] + byte(i&1)
    }
    return
}()

2.7 作用域

  • 作用域和生命周期的区别
  • 句法块是由花括弧所包含的一系列语句, 句法块内部声明的名字是无法被外部块访问的
  • 我们可以把块(block)的概念推广到包括其他声明的群组,这些声明在代码中并未显式地使用花括号包裹起来,我们称之为词法块
  • 一个程序可能包含多个同名的声明,只要它们在不同的词法域就没有关系。
  • 当编译器遇到一个名字引用时,查找过程从最内层的词法域向全局的作用域进行。

for循环类似,if和switch语句也会在条件部分创建隐式词法域,还有它们对应的执行体词法域。下面的if-else测试链演示了x和y的有效作用域范围:

1
2
3
4
5
6
7
8
if x := f(); x == 0 {
    fmt.Println(x)
} else if y := g(x); x == y {
    fmt.Println(x, y)
} else {
    fmt.Println(x, y)
}
fmt.Println(x, y) // compile error: x and y are not visible here

在else if 中可以访问x,但在fmt中无法访问

特别注意短变量声明语句的作用域范围

1
2
3
4
5
6
7
8
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没有被更新,且直接编译错误(unused)

如果改为如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var cwd string

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

错误将难以察觉。

解决的方法是,通过单独声明err变量,来避免使用:=的简短声明方式:

1
2
3
4
5
6
7
8
9
var cwd string

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