《A Tour of Go》笔记

程序结构

按照约定,包名与包路径的最后一个元素相同。例如 math/rand 包中的源码均以 package rand 开始。

可以使用括号来对 import 语句进行分组,对 import 语句进行分组是一种好的风格。

在 Go 中,没有 publicprotectedprivate 关键字。若(变量、函数......)的 名称是以大写字母开头,则表示它们是 exported 的,即可以被外部包中的代码所访问

函数

函数可以接收零个或多个参数。Go 的 类型是位于名称之后 的,至于为什么这么设计,可以看 Go's Declaration Syntax

func add(x int, y int) int {
	return x + y
}

当函数中有两个或者更多连续相同类型的形式参数,则可以省略除最后一个参数之外的所有参数的类型声明。

func add(x, y int) int {
	return x + y
}

函数可以返回任意数量的结果

函数的返回值可以被命名,它们会被视为在函数顶部定义的变量。

变量

var 关键字可以声明一个变量列表,变量的类型也是位于名称之后的。变量声明语句可以位于包级别和函数级别。

变量的声明可以包含初始化值,每个变量对应一个值。变量的声明也可以使用括号来进行分组。

变量的声明中若包含初始化值,则可以省略变量的类型定义。该变量的类型会从初始化值中推导得出

在函数内部,:= 赋值语句可以替换隐式类型声明的变量声明语句。在函数外部,每个语句都必须以 Go 关键字开始(varfunc......),所以不能使用 := 语法。

Go 的基础类型有:

bool

string

int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

byte // alias for uint8

rune // alias for int32
     // represents a Unicode code point

float32 float64

complex64 complex128

没有被明确初始化的变量,会被赋予。数值类型变量的零值是 0,布尔类型变量的零值是 false,字符串类型变量的零值是空字符串。

表达式 T(v) 用于将值 v 转换成类型 T。

在变量初始化的类型推导中,当右侧是没有类型的数值常量时,声明的变量可能会是 intfloat64complex128,这取决于数值常量的精度。

i := 42           // int
f := 3.142        // float64
g := 0.867 + 0.5i // complex128

const 关键字可以声明常量,常量可以是字符、字符串、布尔值、数值。常量不可以使用 := 语法赋值。

控制流程

for

Go 中只有 for 语句一种循环结构。Go 的 for 语句由逗号分隔的三个部分组成:初始化语句、条件语句、后置语句,它们不需要由括号包裹,并且都是可以忽略的。

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println(sum)
}
func main() {
	sum := 1
	for ; sum < 1000; {
		sum += sum
	}
	fmt.Println(sum)
}

for 语句中,当忽略初始化语句和后置语句时,也可以忽略 ; 符号,这便是 Go 中的 where 语句。

func main() {
	sum := 1
	for sum < 1000 {
		sum += sum
	}
	fmt.Println(sum)
}

for 语句中,当连条件语句也被忽略时,则表示无限循环。

if-else

if 语句和 for 语句类似,判断条件也不需要由括号包裹。

if 语句和 for 语句类似,也可以在判断条件之前,执行一个只在当前作用域内有效的变量初始化语句。

if 语句中初始化的变量,在 else 语句中也是有效的。

switch

switch 语句只会执行匹配成功的 case 语句,而不是其后的所有 case 语句。实际上,Go 会在每个 case 语句末尾自动添加 break 语句。

switch 语句和 case 语句的条件语句不需要是常量,它们所涉及的值也不需要是整数。

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
	}
}

switch 语句由上而下执行,在匹配成功时便会结束。在下例中,当 i==0 时,f() 是不会被调用的。

switch i {
	case 0:
	case f():
}

不带条件语句的 switch 语句等价于 switch true,这种写法是另一种简洁的 if-then-else 语句。

func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

defer

defer 语句可以将待执行函数,推迟到当前函数返回之后再执行,但待执行函数的参数是会被立即执行的。

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

defer 语句的待执行函数会被压入到一个栈中,在当前函数返回之后,会按照 Last-In-First-Out 的顺序被执行

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

数据结构

pointer

Go 中也是有指针的。指针保存的是值的内存地址。

类型 *T 表示一个指向类型 T 的指针,指针的零值是 nil

& 运算符用于生成一个指向其操作数的指针。* 运算符用于获取指针指向的值。

func main() {
	i, j := 42, 2701

	p := &i         // point to i
	fmt.Println(*p) // read i through the pointer
	*p = 21         // set i through the pointer
	fmt.Println(i)  // see the new value of i

	p = &j         // point to j
	*p = *p / 37   // divide j through the pointer
	fmt.Println(j) // see the new value of j
}

与 C 语言不同,Go 中是没有指针运算的

structs

结构(struct)是一组字段的集合,使用 . 符号来访问结构的字段。

type Vertex struct {
	X int
	Y int
}

func main() {
	v := Vertex{1, 2}
	v.X = 4
	fmt.Println(v.X)
}

可以通过指向结构的指针来访问结构的字段,Go 中 (*p).X 的简写方式为 p.X,对应了 C 语言中的 p->X

结构字面量表示通过列出字段值的方式,来分配的一个新结构。可以使用 Name: 的语法来仅列出字段的子集。

var (
	v1 = Vertex{1, 2}  // has type Vertex
	v2 = Vertex{X: 1}  // Y:0 is implicit
	v3 = Vertex{}      // X:0 and Y:0
	p  = &Vertex{1, 2} // has type *Vertex
)

array

类型 [n]T 表示一个由 n 个 T 类型的值组成的数组(Array)。数组的长度是其类型的一部分,数组的长度不可以被调整

func main() {
	var a [2]string
	a[0] = "Hello"
	a[1] = "World"
	fmt.Println(a[0], a[1])
	fmt.Println(a)

	primes := [6]int{2, 3, 5, 7, 11, 13}
	fmt.Println(primes)
}

slice

类型 []T 表示一个由 T 类型的值组成的切片(slice)。切片的长度可以被动态调整

切片是通过指定一对上下限的数组下标,并以冒号分隔而组成的。切片会包含上限对应的元素,但不会包含下限对应的元素

func main() {
	primes := [6]int{2, 3, 5, 7, 11, 13}

	var s []int = primes[1:4]
	fmt.Println(s)
}

切片不会存储任何数据,它只是展示了底层数组的一部分数据。修改切片的元素,也会修改底层数组的元素,其它共享底层数组的切片也会看到数据元素的变更

func main() {
	names := [4]string{
		"John",
		"Paul",
		"George",
		"Ringo",
	}
	fmt.Println(names)

	a := names[0:2]
	b := names[1:3]
	fmt.Println(a, b)

	b[0] = "XXX"
	fmt.Println(a, b)
	fmt.Println(names)
}

切面字面量类似于数组字面量,只是不需要指定长度。

// 数组字面量
[3]bool{true, true, false}

// 切片字面量,创建了与上例数组字面量相同的数据,然后创建了引用该数组的切片
[]bool{true, true, false}

创建切片的时候,可以省略上下限的数组下标,并以默认值替代。下限的数组下标默认值为 0,上限的数组下标默认值为数组长度。

var a [10]int
// 以下创建切片语句,都是等价的
a[0:10]
a[:10]
a[0:]
a[:]

切片的长度表示切片包含的元素个数,切片的容量表示切片引用的底层数组的元素个数,从切片的第一个元素开始统计。切片的长度可以使用 len(s) 表达式来获取,切片的容量可以使用 cap(s) 表达式获取。可以通过重新定义切片的上下限来扩展切片的长度,但需要保证切片有足够的容量

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)

	// Slice the slice to give it zero length.
	s = s[:0]
	printSlice(s)

	// Extend its length.
	s = s[:4]
	printSlice(s)

	// Drop its first two values.
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

切片的零值是 nil,零值切片的长度和容量都是 0,并且没有引用的底层数组。

可以使用内置的 make 函数来创建切片make 函数会创建一个元素均为零值的数组,然后返回引用该数组的切片。这便是 Go 中创建动态数组的方式。

func main() {
	a := make([]int, 5)
	printSlice("a", a)

	b := make([]int, 0, 5)
	printSlice("b", b)

	c := b[:2]
	printSlice("c", c)

	d := c[2:5]
	printSlice("d", d)
}

func printSlice(s string, x []int) {
	fmt.Printf("%s len=%d cap=%d %v\n",
		s, len(x), cap(x), x)
}

切片可以包含任何类型,甚至是其它切片。

可以使用内置的 append 函数来给切片追加元素。当被追加的切片容量不够时,append 函数则会创建一个更大的底层数组,并且返回的切片也会引用这个新创建的数组。更多关于切片的细节,可以看 Go Slices: usage and internals

练习

package main

import "golang.org/x/tour/pic"

func Pic(dx, dy int) [][]uint8 {
	var result [][]uint8 = make([][]uint8, dy)
	for y, _ := range result {
		result[y] = make([]uint8, dx)
		for x, _ := range result[y] {
			result[y][x] = uint8((x + y) / 2)
		}
	}
	return result
}

func main() {
	pic.Show(Pic)
}

range

for 循环中,可以使用 range 来遍历切片和映射。当遍历切片的时候,每次迭代会返回两个值,第一个是当前元素的下标,第二个是该下标对应的元素的值副本。

func main() {
    var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}

可以通过赋值 _ 来忽略 range 返回的元素下标或者元素值副本。如果只需要元素下标,则可以忽略第二个返回值。

for i, _ := range pow
for _, value := range pow
for i := range pow

map

映射(map)用于将键(key)映射到值(value)。

映射的零值是 nil,零值映射没有任何 key,也不能添加 key。

可以使用内置的 make 函数来创建映射make 函数会创建一个指定类型的映射,并会为其初始化。

type Vertex struct {
	Lat, Long float64
}

var m map[string]Vertex

func main() {
	m = make(map[string]Vertex)
	m["Bell Labs"] = Vertex{
		40.68433, -74.39967,
	}
	fmt.Println(m["Bell Labs"])
}

映射字面量类似于结构体字面量,但必须要指定键。

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

如果映射的值是一个类型,那么可以在字面量的元素中省略。

type Vertex struct {
	Lat, Long float64
}

var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}

插入或更新映射中的元素:m[key] = elem,获取映射中的元素:elem = m[key],删除映射中的元素:delete(m, key),判断映射中是否存在键:elem, ok = m[key],若键不存在,则 elem 是该类型的零值,并且 ok 值为 false

练习

package main

import (
	"strings"

	"golang.org/x/tour/wc"
)

func WordCount(s string) map[string]int {
	result := make(map[string]int)
	for _, word := range strings.Fields(s) {
		if _, ok := result[word]; ok {
			result[word]++
		} else {
			result[word] = 1
		}
	}
	return result
}

func main() {
	wc.Test(WordCount)
}

function

函数(function)也是值,可以像其它值一样被传递。函数值可以被作为函数的参数和返回值。

Go 中的函数可以是闭包。闭包是一个函数值,它引用了函数体之外的变量。该函数可以访问和赋值它引用的变量。从这个意义上来说,闭包函数是被绑定到变量上的。

func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}

练习:斐波那契闭包

package main

import "fmt"

// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
	a, b := 0, 1
	return func() int {
		fib := a
		a = b
		b = fib + b
		return fib
	}
}

func main() {
	f := fibonacci()
	for i := 0; i < 10; i++ {
		fmt.Println(f())
	}
}

方法

Go 中没有 class,但是类型(Type)可以有方法(Method)。方法是一种带有接收者(Receiver)参数的特殊函数。

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

除了可以给「结构体类型」定义方法,也可以给「非结构体类型」定义方法。但需要注意的是,只能给当前包范围内的类型定义方法。

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

可以给「指针类型」定义方法,接收者为「指针类型」的方法可以修改接收者指针所指向的值。接收者为「值类型」的方法修改的只是值的副本,Go 中对函数参数的操作也是如此。

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

接收者为「指针类型」的方法,既可以被指针所调用,也可以被值所调用。Go 会将 v.Scale(5) 语句解释为 (&v).Scale()。同样的,接收者为「值类型」的方法,也都可以被指针和值所调用,Go 会将 p.Scale(5) 解释为 (*p).Scale(5)

通常都会定义接收者为「指针类型」的方法,主要有两点原因:

  1. 可以修改指针所指向的值;

  2. 避免在调用方法时候产生的值拷贝。

接口

接口(Interface)类型是一组方法签名定义的集合。接口类型的值可以是任何实现了所有这些方法的类型的值。

Go 中不需要「显式声明实现一个接口」,也没有 implements 关键字,实现一个接口只需实现它的所有方法即可。这种隐式的实现接口方式,可以解耦「接口的定义」和「接口的实现」。

在底层实现中,可以把接口类型认为是一个「值 + 具体类型」的元祖 (value, type)。接口值保存的就是接口具体类型的值,调用接口值的方法就是在接口具体类型上调用接口值的同名方法。

值为 nil 的接口

如果接口值为 nil,则调用接口方法时的接收者就是 nil。在一些语言中,这种情况可能会导致一个空指针异常,但在 Go 中,可以优雅地处理这种情况。

type I interface {
	M()
}

type T struct {
	S string
}

func (t *T) M() {
	if t == nil {
		fmt.Println("<nil>")
		return
	}
	fmt.Println(t.S)
}

nil 接口

接口值为 nil 的接口本身并不为 nil,nil 接口本身不包含任何值和类型,并且在 nil 接口上调用方法会导致一个运行时异常。

空接口

不包含任何方法的接口被称为空接口。空接口可以保存任何类型的值,通常被用于处理未知的类型

类型断言

类型断言(Type Assertion)提供了访问接口值底层的具体类型的值的方式。如下,类型断言语句会断言接口值 i 底层的具体类型是否为 T,然后将 T 类型的具体值赋值给变量 t。如果接口值 i 底层的具体类型不是 T,则会抛出一个异常。

t := i.(T)

类型断言可以返回两个值,第二个返回值用于判断接口值底层是否保存了某个具体类型。如下,如果接口值 i 底层的具体类型是 T,则 t 就是具体类型的值,ok 值为 true。如果接口值 i 底层的具体类型不是 T,则 t 是具体类型的零值,ok 值为 false,并且程序不会抛出异常。

t, ok := i.(T)

类型选择

类型选择(Type Switch)是一种按序从多个类型断言中选择分支的结构。

switch v := i.(type) {
case T:
    // here v has type T
case S:
    // here v has type S
default:
    // no match; here v has the same type as i
}

类型选择语句类似于标准的 switch 语句,不过 case 子句中匹配的是具体类型(而不是值),它们会被用于和接口值的具体类型所比较。

类型选择的声明部分类似于类型选择 v :=i.(T),不过具体类型 T 被替换成了关键字 type

常用接口

Stringer

fmt 包中定义了一个 Stringer 接口,用于将接口自身描述为一个字符串。fmt 包和其它很多包会使用 Stringer 接口来打印值。

type Stringer interface {
    String() string
}

Error

error 是 Go 中内建的接口,用于表示程序的错误状态。类似于 fmt.Stringerfmt 包会使用 error 接口来打印错误值。

type error interface {
    Error() string
}

函数通常都会返回一个 error 值,调用函数时则需要判断 error 值是否为 nil,error 值为 nil 表示函数调用成功,error 值不为 nil 表示函数调用失败。

Reader

io 包中定义了一个 Reader 接口,用于读取流中的数据。Go 标准库中有很多 Reader 接口的实现,包括文件读写、网络连接、压缩算法、加密算法等等。

type Reader interface {
	Read(p []byte) (n int, err error)
}

Read 方法会填充给定的字节切片,然后返回填充的字节数量和错误值。错误值 io.EOF 表示已经读取到了流的末尾。

Image

image 包中定义了一个 Image 接口,用于表示一张图片。

type Image interface {
	ColorModel() color.Model
	Bounds() Rectangle
	At(x, y int) color.Color
}

并发

goroutine

协程(goroutine)是由 Go 运行时管理的轻量级线程。

go f(x, y, z) 语句表示启动一个新的协程 f(x, y, z) 并执行。

fxyz 会在当前的协程中求值,f 函数会在新的协程中执行。

多个 Go 协程会在相同的地址空间中运行,因此在协程中访问共享的内存变量时必须保证同步。

channel

管道(channel)是带有类型的渠道,可以使用操作符 <- 在管道上发送和接收数据。

ch <- v    // Send v to channel ch.
v := <-ch  // Receive from ch, and
           // assign value to v.

与映射和切片一样,管道必须先创建,然后才能使用:

ch := make(chan int)

操作符 <- 表示管道的方向(发送或者接收),如果没有指定方向,则表示管道是双向的。管道可以被限制为单向的(只能执行发送或者接收操作)

var ch chan T          // can be used to send and receive values of type T
var ch chan<- float64  // can only be used to send float64s
var ch <-chan int      // can only be used to receive ints

在默认情况下,管道上的发送和接收操作会被阻塞,直到另一侧的操作已经准备就绪。这使得协程在没有显式锁或者条件变量的情况下可以实现同步操作。

管道是可以带缓冲的,把缓冲长度作为第二个参数赋值给 make 函数,就可以创建一个带缓冲的管道。

ch := make(chan int, 100)

当缓冲填满时,向管道发送数据会被阻塞。当缓冲为空时,从管道接收数据会被阻塞。

管道的发送者可以使用 close 函数来关闭管道,用于表示不再发送任何数据。管道的接收者可以通过接收表达式的第二个参数,来判断管道是否已经关闭。当管道已经关闭时,ok 值为 false

v, ok := <-ch

for i := range c 循环语句将会重复地从管道中获取数据,直至该管道被关闭为止。

只有管道的发送者才可以关闭管道,而不是管道的接收者。在一个已关闭的管道上发送数据时,将会导致程序异常。

管道不同于文件,通常不需要被关闭。仅当需要告诉接收者该管道已经不再发送数据的时候,才需要关闭管道,例如终止一个管道的 range 循环。

select

select 语句可以让一个协程等待多个通信操作。select 语句会一直阻塞,直到它的某个 case 语句可以运行为止,然后执行该 case 语句。如果有多个 case 语句准备就绪时,select 语句会随机选择一个来执行。

如果 select 语句中没有准备就绪的 case 语句时,则会执行 default 语句。

sync.Mutex

如果只是想要保证在同一个时刻,只有一个协程可以访问一个共享变量,从而避免产生冲突的话,则需要使用一种叫互斥锁(mutex)的数据结构。

Go 标准库中提供了 sync.Mutex 结构和它的 LockUnlock 方法来实现互斥操作。可以通过调用 LockUnlock 方法来包裹一段代码块,来保证它的互斥执行。也可以通过 defer 语句来保证互斥锁一定会被解开。

参考资料

最后更新于