FantasticMao 技术笔记
BlogGitHub
  • README
  • C & Unix
    • C
      • 《C 程序设计语言》笔记
      • C 语言中的陷阱
      • CMake 示例
      • GNU make
      • LLVM Clang
      • Nginx 常用模块
      • Vim 常用命令
    • Unix-like
      • 《深入理解计算机系统》笔记
      • 《UNIX 环境高级编程》笔记 - UNIX 基础知识
      • 《UNIX 环境高级编程》笔记 - 文件 IO
      • 《UNIX 环境高级编程》笔记 - 标准 IO 库
      • 《鳥哥的 Linux 私房菜》笔记 - 目录配置
      • 《鳥哥的 Linux 私房菜》笔记 - 认识与学习 bash
      • 《鳥哥的 Linux 私房菜》笔记 - 任务管理
      • OpenWrt 中的陷阱
      • iptables 工作机制
  • Go
    • 《A Tour of Go》笔记
    • Go vs C vsJava
    • Go 常用命令
    • Go 语言中的陷阱
  • Java
    • JDK
      • 《Java 并发编程实战》笔记 - 线程池的使用
      • 设计模式概览
      • 集合概览
      • HashMap 内部算法
      • ThreadLocal 工作机制
      • Java Agent
    • JVM
      • 《深入理解 Java 虚拟机》笔记 - Java 内存模型与线程
      • JVM 运行时数据区
      • 类加载机制
      • 垃圾回收算法
      • 引用类型
      • 垃圾收集算法
      • 垃圾收集器
    • Spring
      • Spring IoC 容器扩展点
      • Spring Transaction 声明式事务管理
      • Spring Web MVC DispatcherServlet 工作机制
      • Spring Security Servlet 实现原理
    • 其它
      • 《Netty - One Framework to rule them all》演讲笔记
      • Hystrix 设计与实现
  • JavaScript
    • 《写给大家看的设计书》笔记 - 设计原则
    • 《JavaScript 权威指南》笔记 - jQuery 类库
  • 数据库
    • ElasticSearch
      • ElasticSearch 概览
    • HBase
      • HBase 数据模型
    • Prometheus
      • Prometheus 概览
      • Prometheus 数据模型和指标类型
      • Prometheus 查询语法
      • Prometheus 存储原理
      • Prometheus vs InfluxDB
    • Redis
      • 《Redis 设计与实现》笔记 - 简单动态字符串
      • 《Redis 设计与实现》笔记 - 链表
      • 《Redis 设计与实现》笔记 - 字典
      • 《Redis 设计与实现》笔记 - 跳跃表
      • 《Redis 设计与实现》笔记 - 整数集合
      • 《Redis 设计与实现》笔记 - 压缩列表
      • 《Redis 设计与实现》笔记 - 对象
      • Redis 内存回收策略
      • Redis 实现分布式锁
      • Redis 持久化机制
      • Redis 数据分片方案
      • 使用缓存的常见问题
    • MySQL
      • 《高性能 MySQL》笔记 - Schema 与数据类型优化
      • 《高性能 MySQL》笔记 - 创建高性能的索引
      • 《MySQL Reference Manual》笔记 - InnoDB 和 ACID 模型
      • 《MySQL Reference Manual》笔记 - InnoDB 多版本
      • 《MySQL Reference Manual》笔记 - InnoDB 锁
      • 《MySQL Reference Manual》笔记 - InnoDB 事务模型
      • B-Tree 简述
      • 理解查询执行计划
  • 中间件
    • gRPC
      • gRPC 负载均衡
    • ZooKeeper
      • ZooKeeper 数据模型
    • 消息队列
      • 消息积压解决策略
      • RocketMQ 架构设计
      • RocketMQ 功能特性
      • RocketMQ 消息存储
  • 分布式系统
    • 《凤凰架构》笔记
    • 系统设计思路
    • 系统优化思路
    • 分布式事务协议:二阶段提交和三阶段提交
    • 分布式系统的技术栈
    • 分布式系统的弹性设计
    • 单点登录解决方案
    • 容错,高可用和灾备
  • 数据结构和算法
    • 一致性哈希
    • 布隆过滤器
    • 散列表
  • 网络协议
    • 诊断工具
    • TCP 协议
      • TCP 报文结构
      • TCP 连接管理
由 GitBook 提供支持
在本页
  • 程序结构
  • 包
  • 函数
  • 变量
  • 控制流程
  • for
  • if-else
  • switch
  • defer
  • 数据结构
  • pointer
  • structs
  • array
  • slice
  • range
  • map
  • function
  • 方法
  • 接口
  • 值为 nil 的接口
  • nil 接口
  • 空接口
  • 类型断言
  • 类型选择
  • 常用接口
  • 并发
  • goroutine
  • channel
  • select
  • sync.Mutex
  • 参考资料
  1. Go

《A Tour of Go》笔记

最后更新于1年前

程序结构

包

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

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

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

函数

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

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

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

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

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

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

变量

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

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

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

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

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。

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

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)
}

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

练习

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.Stringer,fmt 包会使用 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) 并执行。

f、x、y、z 会在当前的协程中求值,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 结构和它的 Lock、Unlock 方法来实现互斥操作。可以通过调用 Lock 和 Unlock 方法来包裹一段代码块,来保证它的互斥执行。也可以通过 defer 语句来保证互斥锁一定会被解开。

参考资料

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

Go's Declaration Syntax
Go Slices: usage and internals
A Tour of Go
Go's Declaration Syntax
Go Slices: usage and internals
The Go Programming Language Specification