本文来源于 [A Tour of Go](https://tour.golang.org/welcome/1) 学习过程中的笔记。

基础

每个 Go 程序都是由包组成,每个 Go 文件都仅属于一个包,一个包可以由多个文件组成。
必须在源文件中非注释的第一行指定这个文件属于哪个包 package xxx
程序运行的入口是包 main 。package main 表示一个可独立执行的程序。

1
2
3
4
5
6
7
package main

// 这里可以分开为多个 import 语句,但这里的这种打包的方式更好。
import (
"fmt"
"math/rand"
)

按照惯例,import 中引入的包的包名为路径中的最后一个目录名
在 Go 中,首字母大写的名称用于被导出,只有被导出的名字才能在包外访问,比如 FooFOO

每个源文件可以包含一个 init 函数用于初始化,初始化会按照包的依赖关系顺序单线程执行。


函数

函数可以没有参数或接受多个参数(类型定义位于变量名之后 相关引用

1
2
3
func add(x int, y int) int {
...
}

当多个连续的函数命名参数是同一类型,则可以合并。
x int, y int 可以合并为 x, y int
函数可以返回任意数量的返回值。return x, y
函数的返回值可以被命名(在函数声明中)func split(sum int) (x, y int)
如果 return 语句没有参数,则表示返回所有返回变量的当前值。这种用法被称为“裸(naked)”返回。
尽量在短函数中使用 naked return,因为这可能影响代码的可读性

如果函数的最后一个参数为 ...type 的形式,则表示该函数可以处理变长参数。

1
2
3
4
5
6
7
8
// 这里参数 who 的类型是个 slice:[]string
func Greeting(prefix string, who ...string) {...}

// 如果变参函数的参数存储在一个数组中,则可以使用下面这种方式调用
Greeting(arr...) // arr是一个数组

// 可以使用空接口来接收任意类型的不定参数
func typeCheck(values ... interface{}) {...}


变量

使用 var 定义变量列表,同样的,类型放在后面。例如:var c, python, java bool
var 可以定义在包或函数级别。
变量定义时可以初始化值;因为变量可以在初始值中获得类型,所以变量在声明及初始化时可以省略类型。

1
2
3
4
5
6
var i, j int = 1, 2
var m, n = 1, "hello"
var (
k int = 1
l = 2
)

只有在函数中, 简洁赋值语句 := 在明确类型的地方,可以替代 var 定义。

1
2
3
// 以下代码位于某一函数中
var i, j = 1, 2
m, n := 1, "hello"

函数外的每个语句都必须以关键字开始(varfunc 等等),:= 结构不能用在函数外
函数也是一个值,

基本类型

Go Basic types:

  • bool
  • string
  • int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr
  • byte (uint8 的别名)
  • rune (int32 的别名,代表一个 Unicode 码)
  • float32 float64
  • complex64 complex128

int uint uintptr 类型的位长依赖系统,32位系统上一般为32位,64位系统上一般为64位。

变量在定义时未明确初始化时会自动赋值为 零值,不同类型的零值不同:

  • 数值类型:0
  • 布尔类型:false
  • 字符串:””

不同类型的变量之间赋值时需要显式类型转换,使用表达式 T(v) 可以将 v 的值转换为类型 T

1
2
3
i := 100
j := float64(i)
u := uint(j)

在定义一个变量并不显示指定类型时(使用 :=var =),变量的类型由等号右侧的值推导得出。
如果右侧为一个有类型的变量,则左侧变量类型与其相同;如果右侧变量为非指定类型数字常亮,则取决于其精度。

1
2
3
4
5
6
// 以下代码位于某一函数中
var v = 42 // 也可以使用 v := 42
fmt.Printf("v is of type %T\n", v)
// v := 42 output: int
// v := 3.14 output: float64
// v := 0.434 + 0.5i output: complex64

常量 的定义与变量类似,只不过是使用 const 关键字。
常量的值必须是能够在编译时就能确定的。
常量可以是字符串、字符、bool或数字类型的值。
常量不能用 := 语法定义。
数值常量是高精度的值,一个未指定类型的常量由上下文来决定其类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const (
Big = 1 << 100
Small = Big >> 99
)
func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
return x * 0.1
}

func main() {
fmt.Println(needInt(Small)) // 21
fmt.Println(needFloat(Small)) // 0.2
fmt.Println(needFloat(Big)) // 1.2676506002282295e+29
}

流程控制语句

for 语句
Go 中有且仅有一种循环结构 for [初始化语句]; 循环条件表达式; [后置语句]

1
2
3
for i := 0; i < 10; i++ {
...
}

其中 初始化语句后置语句 均为可选,如不需要,保持为空即可 for ; i < 10 ; {...},此时的 for 语句等同于C语言中的 while。
如果只写 for 则表示死循环,比如:for {...}

if 语句
语法为 if [简单语句;] 条件表达式 {...} [else {...}]
在条件表达式之前可以执行的简单语句,这个语句定义的变量仅在 if - else 范围之内。

1
2
3
if i := 10; i < 20 {
...
}

switch 语句
switch 中的条件(case)从上往下执行,直到匹配成功。
每个 case 代码块的末尾都可以看作有个隐式的 break,如果需要继续执行下一个 case 的代码,则需要在上一个 case 的最后使用 fallthrough

1
2
3
4
5
6
7
8
switch os := runtime.GOOS; os {
case "darwin":
...
case "linux":
...
default:
...
}

没有条件的 switch 语句等同于 switch true。由此可以用较为清晰的方式编写长 if-then-else 链。

defer语句
defer 语句可以延时一个函数的执行直到上层函数返回,被延时调用的函数参数会立刻生成。
被延时的函数调用会被压入一个栈中,当外层函数返回时,会按照 LIFO 的顺序执行被延时的函数调用。

1
2
3
4
5
6
7
8
9
func main() {
for i := 0; i < 10; i++ {
defer fmt.Printf("%v ", i)
}
fmt.Println("done")
}
// output:
// done
// 9 8 7 6 5 4 3 2 1 0

defer 通常用于释放某些资源。

复杂类型

指针
指针保存了变量的内存地址
类型 *T 是指向类型 T 的值的指针。其零值是 nil 。例如:var p *int
& 符号会生成一个指向其作用对象的指针。例如:var p = &i
* 符号表示指针指向的底层的值。例如:*p = 21
不存在任何“指针运算”。

结构体
struct 就是一个字段的集合。
使用点号来访问结构体中的字段。结构体字段也可以通过结构体指针来访问,访问方式依然是用点号。

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X, Y int
}

func main() {
v1 := Vertex{1, 2} // X:1 Y:2
v2 := Vertex{X: 1} // X:1 Y:0
v3 := Vertex{} // X:0 Y:0
v4 := &Vertex{1, 2} // 类型为 *Vertex 的指针
}

数组
类型 [n]T 是一个有 n 个类型为 T 的值的数组。

1
2
3
var a [10]int               // just declare
var b = [3]int{1, 2, 3} // 声明时初始化
var c = [...]int{1, 2, 3} // 编译时自动计算数组大小

数组的大小是数组的一部分,所以不能改变数组大小。

slice more
slice 是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),由指向相关数组的指针、长度(length)以及容量(capacity)组成。由此可以理解为:slice 提供了一个数组的动态窗口。
slice 的容量表示最大长度,0 <= len(s) <= cap(s)
slice 中可以包含任意类型。
对 slice 进行切片会创建一个新的 slice,这两个 slice 指向同一个内部数组。例如:s[start:end]
slice 由函数 make 创建。该函数会创建一个全为 0 的内部数组,并返回一个指向改数组的 slice。
slice 的零值为 nil,此时 lengthcapacity 都为 0。
使用 append 函数向 slice 的末尾添加元素,如果 slice 对应的内部数组太小,则会分配一个更大的数组,并返回指向这个新数组的 slice。

1
2
s := []int{1, 2, 3}
s = make([]int, 3, 3)

range
for 语句中使用 range 可以对 slice 或 map 进行迭代循环。
每次迭代 range 就会返回两个值,一个是下标,一个是下标对应元素。可以使用 _ 来忽略某个值。

1
2
3
4
s := make([]int, 10)
for i, v := range s {
s[i] = 1 << uint(i)
}

map
map 是一键值映射,使用之前必须使用 make 创建,map 的零值为 nil 且不能对其进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Vertex struct {
Lat, Long float64
}

// 仅声明,使用之前必须 make 创建
var m map[string]Vertex
m = make(map[string]Vertex)

// 声明并初始化
var m = map[string]Vertex{
"Bell Labs": { // 这里省略了 Vertex
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}

插入或修改元素 m[key] = value
删除元素 delete(m, key)
通过双赋值检测某个 key 是否存在:elem, ok := m[key],如果 key 存在则 oktrue,否则为 false
当从 map 中读取一个不存在的 key 时,会返回一个元素类型的零值。

方法与接口

方法 Methods
Go 没有类,但是可以在结构体类型上定义方法。
方法接受者(method receiver)出现在 func 关键字和方法名之间。

1
2
3
4
5
6
7
8
9
10
type Vertex struct {
X, Y float64
}

// 这里需要注意 method receiver 为一个指针,所以改方法会修改 receiver 的原始值
// 而 receiver 如果不是指针,则改方法只会修改 receiver 的一个副本。
func (v *Vertex) Abs() float64 {return math.Sqrt(v.X*v.X + v.Y*v.Y)}

v := &Vertex{3, 4}
v.Abs()

可以对当前包内的任意类型定义任意方法,但不能对来自其他包的类型定义方法。

1
2
3
4
type MyFloat float64
func (f MyFloat) Abs() float64 {...}
f := MyFloat(-1.222)
f.Abs()

接口 Interfaces
接口类型是由一组方法来定义。
接口类型的值可以存放任何实现这些方法的值。

1
2
3
4
5
type Abser interface {
Abs() float64
}

var a Abser

Go 中不存在关键字 implements,如果一个类型实现了某个接口指定的所有方法,那么这个类型就实现了这个接口。因此没有必要去显式的声明具体实现哪个接口。
隐式的接口声明将接口的实现与接口的定义完全解耦。

内建接口 Stringer

1
2
3
type Stringer interface {
String() string
}

内建接口 Error

1
2
3
type Error interface {
Error() string
}