Go语言圣经

入门

基础

Go是一门编译型语言

Go语言提供的工具都通过一个单独的命令go调用,go命令有一系列子命令:

  • run子命令编译一个或多个以.go结尾的源文件,如go run helloworld.go
  • build子命令保存编译结果,如go build helloworld.go生成可执行的二进制文件

Go语言代码通过包(package)组织,每个源文件都以一条package声明语句开始,之后跟随导入(import)的包。

main包定义了一个可独立执行的程序,而不是一个库,其内部的main函数是整个程序的执行入口。

规则:

  • 必须恰当导入需要的包,缺少了必要的包或者导入了不需要的包,程序都无法编译通过
  • import声明必须跟在文件的package声明之后
  • Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。编译器会主动把特定符号后的换行符转换为分号, 因此换行符添加的位置会影响Go代码的正确解析。举个例子, 函数的左括号{必须和func函数声明在同一行上, 且位于末尾,不能独占一行,而在表达式x + y中,可在+后换行,不能在+前换行(以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被插入分号分隔符,从而导致编译错误)

案例:

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Hello, 世界")
}

命令行参数

os.Args可以获取程序的命令行参数,其中os.Args[0]是执行命令本身的名字

程序结构

命名

命名规则:以字母或下划线开头,后面跟字母、数字或下划线,大小写敏感。

名字在函数内部定义,则只在函数内部有效。如果在函数外部定义,那么当前包的所有文件都可以访问它(包级可见)。如果一个名字在函数外部定义且以大写字母开头,则其可以被外部的包访问。

推荐使用驼峰式命名法,对于缩略词避免使用大小写混合的写法(如HTML、ASCII)。

声明

var/const/type/func分别对应变量、常量、类型和函数实体对象的声明

变量

变量声明:var 变量名 类型 = 表达式,变量默认使用零值,因此Go语言中不存在未初始化的变量。

多变量声明:var i, j, k int

多初始化表达式:var b, f, s = true, 2.3, "four"

函数返回值初始化:var f, err = os.Open(name)

简短变量声明(类型自动推断)

简短变量声明:i := 1

简短多变量声明:i, j := 0, 1

:=是变量声明语句,而=是变量赋值操作

简短变量声明语句也可以用函数的返回值来声明和初始化变量,f, err := os.Open(name)

简短变量声明语句对已经声明过的变量只有赋值行为,因此语句中必须至少声明一个新的变量,否则不能通过(没有新的变量就用=喽)。

指针

指针与C语言的指针类似,区别是:

  • 可以返回函数中局部变量的地址
  • 指针是变量的别名,Go垃圾回收器会找到变量的全部别名,以判断是否回收

new函数

new(T):创建一个T类型的匿名变量,初始化为T类型的零值,返回变量地址(返回*T),使用较少。

变量的生命周期

对于包一级声明的变量来说,它们的声明周期和整个程序的运行周期一致;对于局部变量,在变量不再被引用后(可达性分析,因为函数中局部变量的地址可以被返回),变量的存储空间可能被回收。

包一级声明的变量必须在堆上分配,而局部变量则由编译器选择在栈上还是堆上分配(堆上分配需要GC回收)

赋值

数值变量也可以支持++--语句(它们是语句,而不是表达式,因此x = i++是错误的)。

在赋值之前,赋值语句右边的所有表达式将会先进行求值,然后再统一更新左边对应变量的值。利用这种方法,实现交换两个变量的值很容易,i, j = j, i

多返回值函数中额外返回布尔值的信息:1)map查找,v, ok = m[key];2)类型断言,v, ok = x.(T);3)通道接受,v, ok = <- ch

_表示不需要的值

for循环

基本的for循环包含三部分:初始化语句,条件判断语句,本次循环后的处理语句。大括号不能省略。

var a [3]int
// 方法一
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}
// 方法二
for i := 0; i < len(a); i++ {
    fmt.Printf("%d %d\n", i, a[i])
}

var b = [...]int{1, 2, 3, 4, 5, 6, 7, 8}
for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
    fmt.Printf("%d %d\n", b[i], b[j])
}

// 当只有条件判断语句时,去掉前后的分号,for就变成了'while'循环。
n := 1
for n < 100 {
    n++
}

// 死循环
for {
    
}

条件语句

if语句的条件判断语句不用小括号,但处理部分一定要用大括号包起来。if语句在条件判断语句之前可以添加一个语句(常常使用的是间短变量声明),其作用域在if和else语句块内。

if v, ok := mp["bob"]; ok {
    
} else if v, ok := mp["alice"]; ok {
    
} else {
    
}
// v, ok的作用域结束

a := 1
if a == 1 {
    fmt.Println("one")
} else if a == 2 {
    fmt.Println("two")
} else {
    fmt.Println("other")
}

switch语句

switch语句从上到下进行匹配,匹配成功则停止。

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语句,相当于 switch true
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.")
}

类型

一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。如type 类型名 底层类型

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

package tempconv

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(t)Fahrenheit(t)是类型转换操作,不是函数调用,不会改变值本身,但是会使它们语义发生变化。

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型(译注:如果T是指针类型,可能会需要用小括弧包装T,比如(*int)(0))。

命名类型还可以为该类型的值定义新的行为。这些行为表示为一组关联到该类型的函数集合,称为类型的方法集。下面的声明语句,Celsius类型的参数c出现在了函数名的前面,表示声明的是Celsius类型的一个名叫String的方法,该方法返回该类型对象c带着°C温度单位的字符串,func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }

包和文件

每个包都对应一个独立的名字空间。如对于Decode函数,需要显示使用image.Decodeutf16.Decode

导入包时可以给一个别名(import fmt "fmt"),默认包名与导入路径的最后一个字段相同。

包的初始化首先是解决包级变量的依赖顺序,然后按照包级变量声明出现的顺序依次初始化:

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

func f() int { return c + 1 }

在包级别声明的变量,如果有初始化表达式则用表达式初始化,没有初始化表达式的可以用特殊的init初始化函数简化初始化工作。init初始化函数不能被调用或引用,每个文件中的init函数在程序开始执行时按照它们声明的顺序被自动调用。

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。因此,如果一个p包导入了q包,那么在p包初始化的时候可以认为q包必然已经初始化过了。初始化工作是自下而上进行的,main包最后被初始化。

作用域

当编译器遇到一个名字引用时,它会对其定义进行查找,查找过程从最内层的词法域向全局的作用域进行。

Go语言的习惯是在if中处理错误然后直接返回,这样可以确保正常执行的语句不需要代码缩进。

注意作用域可能带来的隐晦错误:

var cwd string

// 不能正确初始化全局变量cwd
func init() { // init 函数
    cwd, err := os.Getwd() // wrong! :=语句将cwd和err重新声明为局部变量,内部的cwd屏蔽了全局变量。也就是说,要初始化全局变量时,要避免使用:=变量声明语句,可以先单独声明err变量,再赋值
    /*
    var err error
    cwd, err = os.Getwd()
    */
    if err != nil {
        log.Fatalf("os.Getwd failed: %v", err)
    }
    log.Printf("Working directory = %s", cwd)
}

格式化打印

  • %d, %o, %x, %X:以十进制、八进制、十六进制输出
  • % d等:在每个十进制数之前插入一个空格
  • %#:让Printf函数在用%o、%x或%X输出时生成0、0x或0X前缀
  • %[1] %[2]等:再次使用第一个、第二个操作数
  • %c:打印字符,转义字符会被转义
  • %q:打印带单引号的字符,rune需要用这种方式打印
  • %g:输出足够精度的小数
  • %e:指数形式输出
  • %f:控制宽度和精度,%8.3f表示输出宽度为8,保留三位小数。宽度小于精度长度时忽略宽度
  • %t:打印布尔型数据
  • %T:打印类型信息
  • %s:打印字符串,可以使用%55.10s表示宽度为50,保留字符串前10个字符
  • %v:默认形式打印

基础数据类型

基础数据类型包括数值、字符串、布尔型。

整型

整型类型:

  • 有符号整型有int8、int16、int32、int64
  • 无符号整型有uint8、uint16、uint32、uint64
  • int和uint与操作系统的位数有关,类似于C语言
  • Unicode字符rune类型是和int32等价的类型,通常用于表示一个Unicode码点
  • byte是uint8类型的等价类型
  • 无符号整数类型uintptr,没有指定具体的bit大小但是足以容纳指针

取模运算:%运算符的符号和被取模数的符号总是一致的,因此-5%3-5%-3结果都是-2。

浮点数

浮点数类型:float32、float64,规范为IEEE754浮点数国际标准定义。

最大值:math.MaxFloat32/math.MaxFloat64

NaN:math.IsNaN用于测试一个数是否是非数NaN,math.NaN返回非数对应的值,两个NaN值之间比较的结果是不确定的,即使是同一个NaN自身相比。

复数

复数类型:complex64、complex128,分别对应float32和float64两种浮点数精度。

相关函数:complex函数创建复数,realimag分别返回复数的实部和虚部。如果一个浮点数或者十进制整数后面跟着i,则其将构成一个复数的虚部,实部为0,。

布尔型

布尔型只有true和false两种类型,不会隐式转换为数字值0或1。

字符串

字符串是不可变的,因此s[0] = 'a'是不被编译器允许的,其带来的好处是字符串切片可以共享内存,而不需要分配新的内存。

字符串操作:

  • 切片s[i:j],基于原始的字符串s的第i个字节开始到第j个字节(并不包含j本身)生成一个新字符串,共包含j-i个字节。
  • len:返回一个字符串中的字节数目

原生字符串:使用反引号,不会转换转义字符。

UTF-8编码

  • 定义:UTF8编码使用1到4个字节来表示每个Unicode码点,ASCII部分字符只使用1个字节,常用字符部分使用2或3个字节表示。每个符号编码后第一个字节的高端bit位用于表示编码总共有多少个字节。如果第一个字节的高端bit为0,则表示对应7bit的ASCII字符,和传统的ASCII编码兼容。如果第一个字节的高端bit是110,则说明需要2个字节;后续的每个高端bit都以10开头。

    0xxxxxxx                             runes 0-127    (ASCII)
    110xxxxx 10xxxxxx                    128-2047       (values <128 unused)
    1110xxxx 10xxxxxx 10xxxxxx           2048-65535     (values <2048 unused)
    11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  65536-0x10ffff (other values unused)
    
  • Unicode码点表示形式:\uhhhh对应16bit的码点值,\Uhhhhhhhh对应32bit的码点值,其中h是一个十六进制数字。去掉高端位后剩余的都是码点。

    "世"
    "\xe4\xb8\x96"  // 二进制:11100100 10111000 10010110
    "\u4e16" // 3个字节,去掉高端位后剩余的是 0100 111000 010110,从后往前每4位表示成一个h
    "\U00004e16"
    
    "\xcd\x9d" //  二进制:11001101 10011101
    "\035d" // 2个字节,去掉高端位后剩余的是 01101 011101,从后往前每4位表示成一个h
    
  • rune字符中表示形式:小于256的码点值可以写在一个十六进制转义字节中,如\x41对应字符'A',但对于更大的码点则必须使用\u\U转义形式。因此,\xe4\xb8\x96不是一个合法的rune字符。

    '世'
    '\u4e16'
    '\U00004e16'
    
  • Unicode和rune字符转换

    s := "プログラム"
    // % x参数用于在每个十六进制数字前插入一个空格
    fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
    r := []rune(s) // string转[]rune
    fmt.Printf("%x\n", r)  // "[30d7 30ed 30b0 30e9 30e0]"  \x的形式
    
    fmt.Println(string(r)) // []rune转string
    // 将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8字符串:
    fmt.Println(string(65))     // "A", 不是"65"
    fmt.Println(string(0x4eac)) // "京"
    

对于混合了中文和英文的字符串,使用len返回其占的字节数,使用utf8.RuneCountInString返回Unicode字符数。

当遇到错误的UTF8编码输入,将生成一个特别的Unicode字符\uFFFD

字符串和Byte:

  • 字符串和字节slice的区别是,字符串底层维护只读字节数组,而字节slice可以修改

  • 字符串和字节slice转换:通常会分配一个新的字节数组用于保存字符串数据的拷贝,在字节数组不会改变的情况下,编译器可能会优化以避免分配和复制字符串数据。将字节slice转换到字符串则构造一个字符串拷贝,以确保得到字符串是只读的。

    s := "abc"
    b := []byte(s)
    b[1] = 99
    s2 := string(b)
    
  • bytes.Buffer是字节slice缓存(类似Java的StringBuilder),可以动态增长。当向bytes.Buffer添加UTF8编码的字符时,最好使用WriteRune方法;添加ASCII字符时,使用WriteByte方法更有效。

字符串和数字转换:

  • 整数转为字符串

    • 使用格式化字符串fmt.Sprintf("%d", x)
    • 使用strconv.Itoa
    • 使用strconv.FormatInt(整数, 进制)/strconv.FormatUint,以不同的进制格式化数字
  • 字符串转为整数

    • 使用y, err := strconv.Atoi
    • 使用y, err := strconv.ParseInt(字符串, 进制, 指定整型数的大小)

常量

常量表达式的值在编译期计算,因此常量间的所有算术运算、逻辑运算和比较运算的结果都是常量。

批量声明常量:

const (
    e  = 2.71828182845904523536028747135266249775724709369995957496696763
    pi = 3.14159265358979323846264338327950288419716939937510582097494459
)

批量声明的常量,除了第一个外其它常量右边的初始化表达式可以省略,如果省略则表示使用前面常量的初始化表达式写法,对应的常量类型也相同。

const (
    a = 1
    b
    c = 2
    d
)
fmt.Println(a, b, c, d) // "1 1 2 2"

iota常量生成器:iota初始为0,在每一个有常量声明的行加1。如:

const (
    _ = 1 << (10 * iota) // 1 << 0
    KiB // 1024, 1 << 10
    MiB // 1048576, 1 << 20
    GiB // 1073741824, 1 << 30
)

无类型常量:

  • 分为无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串
  • 编译器为没有明确基础类型的数字常量提供比基础类型更高精度的算术运算
  • 可以直接用于更多的表达式而不需要显式的类型转换
  • 无类型常量被赋值给变量时,无类型的常量将会被隐式转换为对应的类型,如果变量没有显式类型声明,那么常量的形式将决定变量的默认类型(可以通过%T查看无类型常量的默认类型)

复合数据类型

复合数据类型包括数组、slice、map、结构体。

数组

数组是一个固定长度的特定类型元素组成的序列,和数组对应的类型是Slice(切片),它是可以增长和收缩的动态序列。

var a [3]int // 数组声明,初始化为默认值
fmt.Println(a[0]) // 通过索引下标访问
fmt.Println(len(a)) // 数组长度

// 打印索引下标和对应元素
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}

// 默认情况下,数组每个元素初始化为对应元素类型的零值
var q [3]int = [3]int{1, 2}
fmt.Println(q[2]) // 0

// 根据初始化值的个数确定数组长度
q = [...]int{1, 2, 3}
// q = [...]int{1, 2} // Cannot use '[...]int{1, 2}' (type [2]int) as the type [3]int

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。

指定索引和对应值列表的方式初始化数组:

type Currency int

const (
    USD Currency = iota // 美元 0
    EUR                 // 欧元 1
    GBP                 // 英镑 2
    RMB                 // 人民币 3
)

symbol := [...]string{USD: "$", EUR: "€", GBP: "£", RMB: "¥"}

fmt.Println(RMB, symbol[RMB]) // "3 ¥"

// 没有用到的索引可以省略
r := [...]int{99: -1} // 100个元素的数组,最后一个元素初始化为-1

如果一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,只有当两个数组的所有元素都是相等的时候数组才是相等的。

Go语言的函数调用时,形参都是原始参数的一份副本,需要拷贝,对于数组来说非常低效,因此通过数组指针来避免拷贝,对其进行修改会反应到原数组上,局限是由于数组的类型包含了长度信息,只能处理特定大小的数组,所以常用slice来代替数组。

func zero(ptr *[32]byte){ // 清零数组
    *ptr = [32]byte{}
}

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

Slice的结构:

  • 指针:指向第一个slice元素对应的底层数组元素的地址,注意slice的第一个元素并不一定就是数组的第一个元素(切片操作,多个slice之间共享底层数据)。
  • 长度:len函数返回长度,表示slice中元素的数目。
  • 容量:cap函数返回容量,表示从slice的开始位置到底层数据的结尾位置的长度。

如果切片操作超出容量将导致一个panic异常,但是超出长度则是意味着扩展了slice,因为新slice的长度会变大:

count := [...]int{1, 2, 3, 4, 5, 6}
half := count[:3]
extendHalf := half[:5]
fmt.Println(half, extendHalf) // [1 2 3] [1 2 3 4 5]

slice之间不能比较,因此不能使用==操作符来判断两个slice是否含有全部相等元素。标准库提供了高度优化的bytes.Equal函数来判断两个字节型slice是否相等([]byte),但是对于其他类型的slice,我们必须自己展开每个元素进行比较。slice之间不能比较的原因是:1)一个slice的元素是间接引用的,可以包含自身,情形比较复杂;2)slice的元素是间接引用的,一个固定的slice值,在不同的时刻可能包含不同的元素,因为底层数组的元素可能会被修改。

slice唯一合法的比较操作是和nil比较:

var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil

if s == nil { /* */ }
// 需要测试一个slice是否是空的,使用len(s) == 0来判断,而不要用s == nil来判断

make函数:创建一个指定元素类型、长度和容量的slice,容量部分可以省略。make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。

make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]

append函数

append函数可以追加多个元素,甚至追加一个slice:

var x []int
x = append(x, 4, 5, 6)
x = append(x, x...) // 追加slice,省略号表示接收变长的slice

string和slice转换

string和slice互相转换,使用深拷贝,这是由string不可变和slice可变的语义决定的。
如果两者要互转,但是数据量很多,需要考虑使用零拷贝互转。具体通过反射实现,需要注意的是,如果对从string转换的slice进行了修改,则会发生panic。此外,如果拿到转换结果后,又将它返回,则可能产生问题,比如slice被回收了,但转换的string给了外面,其指向的空间已经不在了。

func BytesCastToString(bs []byte) (str string) {
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
	stringHeader.Data = sliceHeader.Data
	stringHeader.Len = sliceHeader.Len
	return str
}

func StringCastToBytes(str string) (bs []byte) {
	stringHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
	sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&bs))
	sliceHeader.Data = stringHeader.Data
	sliceHeader.Len = stringHeader.Len
	sliceHeader.Cap = stringHeader.Len
}

Map

map类型为map[K]V。

创建map:

// 创建空map
mp := map[string]int{}

// 使用make创建空map
mp = make(map[string]int)

// 指定一些值进行创建
mp = map[string]int{
    "alice":   31,
    "charlie": 34,
}

新增和修改:mp["alice"] = 22

删除:delete(mp, "alice")

遍历:

for name, age := range mp {
    fmt.Printf("%s\t%d\n", name, age)
}

查找map失败时,将返回value类型对应的零值,因此mp["bob"] = mp["bob"] + 1, mp["bob"] += 1, mp["bob"]++都可正常执行。如果需要知道map中是否存在元素,则需要判断返回的0表示的是存储的元素就是0,还是不存在而返回的0,因此通过if v, ok := mp["bob"]; !ok { ... }来判断和进一步操作。

Go禁止对map中的value进行取址,这是因为map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

map类型的零值是nil,也就是没有引用任何哈希表,len(mp) == 0, mp == nil

结构体

结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。

结构体定义:

type Employee struct {
    ID        int
	Name, Address string
    DoB       time.Time
    Position  string
    Salary    int
    ManagerID int
}

访问结构体成员:

var emp Employee
// 点操作符
emp.Salary = 3000

// 对成员取地址,再通过指针访问
position = &emp.Position
*position = "Senior " + *position

// 点操作符与指向结构体的指针一起使用
var ptr *Employee = &emp
ptr.Position += " (proactive team player)" // 等价于 (*ptr).Position += " (proactive team player)"

结构体不能包含其自身类型,但可以包含自身的指针类型成员(树和链表)。

结构体字面值:

type Pair struct{ X, Y int}

// 方法一:需要按照成员的类型和顺序进行初始化
pair := Pair{1, 2}
// 方法二:以成员名字和相应的值来初始化,可以包含部分或全部的成员,没有初始化的使用默认值
// 必须是导出的成员,如果是未导出的成员不能使用这种方式,并且方法一和方法二不能混用
pair = Pair{X: 3, Y: 4}

结构体通常使用指针来处理:

p1 := &Pair{1, 2}
// 等价于
p1 := new(Pair) // new初始化一块空间,并返回该空间的地址
*p1 = Pair{1, 2}

结构体使用==比较两个结构体的每个成员,因为是可比较的,所以结构体可作为map的key。

匿名成员指的是只声明一个成员对应的数据类型而不指名成员的名字,其数据类型必须是命名的类型或指向一个命名类型的指针,不能同时包含两个类型相同的匿名成员。匿名嵌入的特性可以使访问嵌套结构体更方便。

type Point struct {
	X, Y int
}

type Circle struct {
	Point
	Radius int
}

type Wheel struct {
	Circle
	Spokes int
}

var w Wheel
// 匿名成员的名字就是命名类型的名字,这种间短的表示方法是语法糖
w.X = 8 // 等同于 w.Circle.Point.X = 8
w.Y = 8 // 等同于 w.Circle.Point.Y = 8
w.Radius = 5 // 等同于 w.Circle.Radius = 5
w.Spokes = 20

// 结构体字面值
// 方法一:按照顺序初始化
w = Wheel{Circle{Point{8, 8}, 5}, 20}
// 方法二:按照名字和值初始化
w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20, // NOTE: trailing comma necessary here (and at Radius)
}

JSON

将结构体转为JSON的过程叫编组(marshaling),且只有导出的成员才会被编码,通过函数json.Marshal完成;将JSON转为结构体的过程叫做unmarshaling,通过函数json.Unmarshal完成,相对应的成员会被填充,其他成员被忽略。

在编解码的过程中,可以使用结构体的成员Tag,其在编译阶段关联到该成员的元信息字符串(即在JSON中的表示是该元信息字符串,而不是结构体的名称)。Tag还可以带一个omitempty选项,表示当结构体成员为空或零值时不生成该成员的JSON对象。

type Movie struct {
    Title  string // 只有导出的成员才会被编码,必须用大写
    Year   int  `json:"released"` // Year在编码后变成released
    Color  bool `json:"color,omitempty"` // Color在编码后变成color,由于用了omitempty,当它是空值或默认值(这里是false)时,不会输出该成员
    Actors []string
}

var movies = []Movie{
    {Title: "Casablanca", Year: 1942, Color: false,
        Actors: []string{"Humphrey Bogart", "Ingrid Bergman"}},
    {Title: "Cool Hand Luke", Year: 1967, Color: true,
        Actors: []string{"Paul Newman"}}
}

data, err := json.Marshal(movies)
// data, err := json.MarshalIndent(movies, "", "    ") // 带两个额外的字符串,每一行输出的前缀和每一个层级的缩进
if err != nil {
    log.Fatalf("JSON marshaling failed: %s", err)
}
fmt.Printf("%s\n", data)

流式编解码器json.Encoder/json.Decoder,根据输入流或输出流进行编解码。

函数

所有的函数形参,都是传递值,go语言中没有传递引用。比如传一个slice,其实是把slice的结构体传过去了。

函数声明

函数声明包括函数名、形参列表、返回值列表、函数体。

// 函数声明
func name(parameter-list) (result-list) {
    body
}

// 返回一个无名变量,返回值列表的括号可以省略
func add(x int, y int) int   {return x + y}
// 形参或返回值有相同的类型时,不必为每个形参都写出参数类型。有名返回值会将局部变量初始化为0,返回时返回对应的局部变量的值。如果一个函数所有返回值都有显式的变量名,则return语句可以没有操作数,称为bare return,但不宜滥用。
func sub(x, y int) (z int)   { z = x - y; return}
// blank identifier(即_)表示强调不使用某个参数。对于多返回值,如果某个值不使用,也可以使用blank identifier来接收
func first(x int, _ int) int { return x }
func zero(int, int) int      { return 0 }
// 无返回值,省略返回值列表
func show() { fmt.Println("hello") }

函数调用必须按照声明顺序为所有参数提供实参,这部分和Java类似,没有默认参数,也不能通过参数名指定形参。

没有函数体的声明表示该函数不是以Go实现的,如append函数。

错误

错误处理策略:

  • 传播错误。某个函数调用失败,则将错误返回给调用者。
  • 重试。对于错误的发生是偶然的,或由不可预知的问题导致的,重试时,需要限制重试的时间间隔或重试的次数,尝试恢复。
  • 输出错误信息并结束程序。当错误发生导致程序无法运行时使用。
  • 输出错误信息但不中断程序。如使用log包或者标准错误流。
  • 忽略错误。

Go语言错误处理风格为:处理失败的逻辑代码放在处理成功的代码之前,成功的逻辑代码一般不放在else中。

文件结尾错误io.EOF,在读取到文件结束后返回的错误。

匿名函数

函数值字面量(func关键字后面没有函数名)是一种表达式,它的值被称为匿名函数。

如果匿名函数在函数中定义,那么匿名函数可以使用该函数的变量。

func squares() func() int { // 返回一个匿名函数
    var x int // x的生命周期不是由作用域决定的,squares返回后,变量x仍然隐式存在
    return func() int { // 匿名函数可以使用函数中的变量
        x++
        return x * x
    }
}
func main() {
    f := squares() 
    fmt.Println(f()) // "1"
    fmt.Println(f()) // "4"
    fmt.Println(f()) // "9"
}

如果匿名函数需要递归调用,则必须先声明变量,再将匿名函数赋值给变量。

var dfs func(n int)
dfs = func(n int) {
    if n == 0 {
        return
    }
    fmt.Println(n)
    dfs(n - 1)
}

可变参数

在参数的类型之前加上...,表示可变参数。

// vals被看作是类型为[] int的切片,函数可以接收任意数量的int型参数
func sum(vals ...int) (total int) {
    for _, val := range vals {
        total += val
    }
    return
}

fmt.Println(sum(0))
fmt.Println(sum(1, 2, 3, 4))

values := []int{1, 2, 3, 4}
fmt.Println(values...) // 原始参数如果是切片类型,只需在最后一个参数后加上省略符

Deferred函数

延迟函数(在程序执行完时进行调用,多个延迟函数的顺序与声明顺序相反,可以认为放在了栈中)

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

// defer和匿名函数混用时,需要谨慎使用
func bigSlowOperation() {
    defer trace("bigSlowOperation")() // 如果没有这个括号,表示trace函数被defer,其会在10秒后被调用;而有了括号,表示匿名函数被defer,因此enter会在进入时就执行,10秒后执行匿名函数
    // ...lots of work…
    time.Sleep(10 * time.Second) // simulate slow operation by sleeping
}
func trace(msg string) func() {
    start := time.Now()
    log.Printf("enter %s", msg)
    return func() { 
        log.Printf("exit %s (%s)", msg,time.Since(start)) 
    }
}

// 对匿名函数使用defer机制,可观察函数的返回值
func double(x int) (result int) {
    defer func() { fmt.Printf("double(%d) = %d\n", x,result) }() // 最后的括号表示执行函数
    return x + x
}

Panic异常

运行时错误(数组访问越界、空指针引用等)会引起panic异常。panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer机制)。随后,程序崩溃并输出日志信息。日志信息包括panic value(通常是某种错误信息)和函数调用的堆栈跟踪信息。

通过调用内置的panic函数也可以引发panic异常。

Recover捕获异常

通常不应该对panic异常做任何处理,不过有时候我们可以在程序崩溃前,做一些操作。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。在未发生panic时调用recover,recover会返回nil。

func work() {
    defer func() {
        if p := recover(); p != nil {
            err = fmt.Error("error occurs: %v", p)
        }
    }()
    // work...
}

方法

一个方法是一个和特殊类型关联的函数(OOP术语的话,就是对象的方法)。

方法相对于函数的优势:

  • 在包外调用比函数调用的形式简单(函数调用需要带上包名)
  • 不同的类型可以有相同的方法名
  • 编译器会根据需要对指针类型和非指针类型做隐式转换(插入&或者*

方法声明

函数声明时,在其名字之前放一个变量,就是一个方法,该变量(称为方法的接收器receiver)会将该函数附加到这种类型上,相当于为这种类型定义了一个独占方法。

type Point struct{ X, Y float64 }

// 函数
func Distance(p, q Point) float64 {
    x := p.X - q.X
    y := p.Y - q.Y
    return x * x + y * y
}

// 方法
func (p Point) Distance(q Point) float64 {
    x := p.X - q.X
    y := p.Y - q.Y
    return x * x + y * y
}

p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // 函数调用
fmt.Println(p.Distance(q))  // 方法调用

Go语言可以给同一个包内的任意命名类型(type关键字)定义方法,只要这个命名类型的底层类型不是指针或者interface。

基于指针对象的方法

一般约定如果某个类型有一个指针作为接收器的方法,那么所有该类型的方法都必须有一个指针接收器,即使是那些并不需要这个指针接收器的函数。

不管你的方法的receiver是指针类型还是非指针类型,都是可以通过指针/非指针类型进行调用的,编译器会帮你做类型转换。

在声明一个方法的receiver该是指针还是非指针类型时,需要考虑两方面的因素,第一方面是这个对象本身是不是特别大,如果声明为非指针变量时,调用会产生一次拷贝;第二方面是如果用指针类型作为receiver,那么要注意,这种指针类型指向的始终是一块内存地址。

// receiver是指针类型
func (p *Point) ScaleBy(factor float64) {
    p.X *= factor
    p.Y *= factor
}

// 调用方法的形式一
r := &Point{1, 2}
r.ScaleBy(2)
// 调用方法的形式二
p := Point{1, 2}
pptr := &p
pptr.ScaleBy(2)
// 调用方法的形式三
p.ScaleBy(2) // 编译器隐式转换:(&p).ScaleBy(2)

// receiver是非指针类型
pptr.Distance(q) // 编译器隐式转换:(*pptr).Distance(q)

通过嵌入结构体来扩展类型

对于外部结构体来说,嵌入结构体的方法也可以像访问嵌入结构体的字段一样直接访问。

type ColoredPoint struct {
	*Point
	Color color.RGBA
}

blue := color.RGBA{0, 0, 255, 255}
p := ColoredPoint{&Point{5, 4}, blue}
p.ScaleBy(2) // 调用Point的ScaleBy方法

方法值和方法表达式

方法值指的是调用方法时,只传接收器,但不传入参数,然后通过变量接收。相当于方法变成了函数被变量接收。

方法表达式其实是将方法作为函数返回,方法的receiver作为函数的第一个参数,其他参数后面跟着。

point := Point{2, 3}
// 方法值
scalaP := p.ScaleBy
scalaP(2)

// 方法表达式
scale := (*Point).ScaleBy
scale(&point, 2) // 是函数而不是方法了,因此不能用内嵌结构体的方式进行调用

接口

接口类型是对其它类型行为的抽象和概括。Go语言中接口类型的独特之处在于它是满足隐式实现的,也就是说,我们没有必要对于给定的具体类型定义所有满足的接口类型;简单地拥有一些必需的方法就足够了。

接口约定

接口类型是一种抽象的类型。它不会暴露出它所代表的对象的内部值的结构和这个对象支持的基础操作的集合,而只暴露方法。

接口约定指的是接口类型有特定的签名和行为函数,如果一个函数接受接口类型,那么任何满足接口签名和行为函数的值都可以在该函数中工作,因为该函数不会对具体操作的值做任何假设。

接口类型

实现接口描述的一系列方法的具体类型是该接口的实例。

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

// 接口组合(接口内嵌)
type ReadCloser interface {
    Reader
    Closer
}
// 接口组合的混合风格
type ReadCloser interface {
    Read(p []byte) (n int, err error)
    Closer
}

实现接口的条件

表达一个类型属于某个接口只要这个类型实现了这个接口即可。

var w io.Writer
w = os.Stdout           // OK: *os.File has Write method
w = new(bytes.Buffer)   // OK: *bytes.Buffer has Write method
w = time.Second         // compile error: time.Duration lacks Write method

var rwc io.ReadWriteCloser
rwc = os.Stdout         // OK: *os.File has Read, Write, Close methods
rwc = new(bytes.Buffer) // compile error: *bytes.Buffer lacks Close method

// 同样适用于等式右边本身也是一个接口类型
w = rwc                 // OK: io.ReadWriteCloser has Write method
rwc = w                 // compile error: io.Writer lacks Close method

接口类型封装和隐藏具体类型和它的值。即使具体类型有其它的方法,也只有接口类型暴露出来的方法会被调用到。

os.Stdout.Write([]byte("hello")) // OK: *os.File has Write method
os.Stdout.Close()                // OK: *os.File has Close method

var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer has Write method
w.Close()                // compile error: io.Writer lacks Close method

空接口interface{},空接口类型对实现它的类型没有要求,所以可以将任意一个值赋给空接口类型。

var any interface{}
any = true
any = 12.34
any = "hello"
any = map[string]int{"one": 1}
any = new(bytes.Buffer)

接口值

接口值由一个具体的类型和该类型的值组成,被称为接口的动态类型和动态值。

var w io.Writer // 零值,类型和值都为nil
w = os.Stdout // 类型为*os.File,值为指向文件描述符的指针
w = new(bytes.Buffer) // 类型为*bytes.Buffer,值为缓冲区的指针
w = nil // 类型和值都为nil

接口值可以使用==和!=进行比较,两个接口值相等仅当它们都是nil,或者它们的动态类型相同并且动态值也相等。因为接口值是可比较的,所以它们可以用在map的键或者作为switch语句的操作数。

注意:如果两个接口值的动态类型相同,但是这个动态类型是不可比较的(如切片、映射类型和函数),将它们进行比较就会发生panic异常。

一个不包含任何值的nil接口值和一个刚好包含nil指针的接口值是不同的。

const debug = true

func main() {
    var buf *bytes.Buffer
    if debug {
        buf = new(bytes.Buffer) // enable collection of output
    }
    f(buf) // NOTE: subtly incorrect!
    if debug {
        // ...use buf...
    }
}

// If out is non-nil, output will be written to it.
func f(out io.Writer) {
    // ...do something...
    if out != nil {
        out.Write([]byte("done!\n"))
    }
}

// 当我们不需要输出时,会将debug设置为false,并认为不会输出。然而,在 if out != nil 出发生panic异常,这是因为out的动态类型是*bytes.Buffer,而动态值是nil,此时out不为nil。此外,发生panic异常是因为nil对于*bytes.Buffer不是一个有效的receiver,而对于其他的类型如*os.File则是一个有效的receiver,但是不管怎么说,这样写的结果可能产生与预期不同的结果。

sort.Interface接口

Go使用一个接口类型sort.Interface来实现排序,该接口的实现由序列的具体表示和它希望排序的元素决定,序列的表示经常是一个切片:

package sort

// sort.Interface的三个方法
type Interface interface {
    Len() int // 序列的长度
    Less(i, j int) bool // 两个元素比较的结果
    Swap(i, j int) // 交换两个元素的方式
}

对字符串切片进行排序:

func main() {
	names := StringSlice{"bob", "alice", "dd"}
	sort.Sort(names)
    /* 或者先将 []string转换为StringSlice
    names := []string{"bob", "alice", "dd"}
	sort.Sort(StringSlice(names))
    */
	fmt.Println(names)
}

type StringSlice []string
func (p StringSlice) Len() int           { return len(p) }
func (p StringSlice) Less(i, j int) bool { return p[i] < p[j] }
func (p StringSlice) Swap(i, j int)      { p[i], p[j] = p[j], p[i] }

Go分别提供了Strings函数、Ints函数实现字符串数组和int数组的排序,isSorted函数用于检查数组是否有序,并提供Reverse函数实现逆序,Reverse函数的原理为:

package sort

type reverse struct{ Interface } // that is, sort.Interface

func (r reverse) Less(i, j int) bool { return r.Interface.Less(j, i) } // i和j反转

func Reverse(data Interface) Interface { return reverse{data} } // 暴露Reverse函数,其内部的Len和Swap由原来的sort.Interface提供

多维排序:

func main() {
	var tracks = []*Track{
		{"Go", "Delilah", "From the Roots Up", 2012, length("3m38s")},
		{"Go", "Moby", "Moby", 1992, length("3m37s")},
		{"Go Ahead", "Alicia Keys", "As I Am", 2007, length("4m36s")},
		{"Ready 2 Go", "Martin Solveig", "Smash", 2011, length("4m24s")},
	}
	sort.Sort(customSort{tracks, func(x, y *Track) bool {
		if x.Title != y.Title {
			return x.Title < y.Title
		}
		if x.Year != y.Year {
			return x.Year < y.Year
		}
		if x.Length != y.Length {
			return x.Length < y.Length
		}
		return false
	}})
	for _, v := range tracks {
		fmt.Printf("%s %s %s %d %s\n", v.Title, v.Artist, v.Album, v.Year, v.Length)
	}
}

type Track struct {
	Title  string
	Artist string
	Album  string
	Year   int
	Length time.Duration
}

func length(s string) time.Duration {
	d, err := time.ParseDuration(s)
	if err != nil {
		panic(s)
	}
	return d
}

type customSort struct {
	t    []*Track
	less func(x, y *Track) bool
}

func (x customSort) Len() int           { return len(x.t) }
func (x customSort) Less(i, j int) bool { return x.less(x.t[i], x.t[j]) }
func (x customSort) Swap(i, j int)      { x.t[i], x.t[j] = x.t[j], x.t[i] }

类型断言

类型断言是使用在接口值上的操作,使用语法为x.(T),其中x表示接口类型而T表示一个类型,类型断言就是检查两者是否匹配:

  • 如果T是具体类型,则类型断言检查x的动态类型是否和T相同
  • 如果T是接口类型,则类型断言检查x的动态类型是否是否满足T
// T是具体类型
var w io.Writer
w = os.Stdout
fmt.Println(w.(*os.File)) // success
fmt.Println(w.(*bytes.Buffer)) // panic: io.Writer is *os.File, not *bytes.Buffer
// T是接口类型
rw := w.(io.ReadWriter) // success: *os.File has both Read and Write
w = new(ByteCounter)
rw = w.(io.ReadWriter) // panic: *ByteCounter has no Read method
// 可以返回两个值,第二个值表示是否成功
f, ok := w.(*bytes.Buffer)

基于类型断言区别错误类型,如os.IsExist函数的实现:

func IsNotExist(err error) bool {
	return underlyingErrorIs(err, ErrNotExist) // ErrNotExist = fs.ErrNotExist
}
func underlyingErrorIs(err, target error) bool {
	// Note that this function is not errors.Is:
	// underlyingError only unwraps the specific error-wrapping types
	// that it historically did, not all errors implementing Unwrap().
	err = underlyingError(err)
	if err == target {
		return true
	}
	// To preserve prior behavior, only examine syscall errors.
	e, ok := err.(syscallErrorType)
	return ok && e.Is(target)
}
// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
	switch err := err.(type) {
	case *PathError:
		return err.Err
	case *LinkError:
		return err.Err
	case *SyscallError:
		return err.Err
	}
	return err
}

通过类型断言询问行为,如io.WriteString函数:

// WriteString writes the contents of the string s to w, which accepts a slice of bytes.
// If w implements StringWriter, its WriteString method is invoked directly.
// Otherwise, w.Write is called exactly once.
func WriteString(w Writer, s string) (n int, err error) {
	if sw, ok := w.(StringWriter); ok { // 询问是否实现了StringWriter,如果实现了则调用其WriteString方法
		return sw.WriteString(s) // WriteString方法可以避免临时拷贝,效率高
	}
	return w.Write([]byte(s))
}

类型开关

类型开关(type switch),对类型进行switch。

接口有两种使用方式:

  1. 接口提供方法,重点在于接口的方法,而不是具体的类型。
  2. 利用接口值可以持有各种具体类型值的能力,将该接口认为是这些类型的union,重点在于具体的类型满足该接口,而不是在于接口的方法,并且不隐藏任何信息。这称为discriminated unions(可辨识联合)。

例子:

// 注意,虽然x的类型是interface{},但是可以认为是一个int, uint, bool, string和nil值构成的可识别联合
switch x := x.(type) { // 重用变量名。和switch语句类似,类型开关隐式的创建了一个语言块,因此新变量x的定义不会和外面的x变量冲突。
	case nil: // ...
    case int, uint: // ...
    case bool: // ...
    case string: // ...
    default: // ...
}

Goroutines和Channels

Goroutines

每一个并发的执行单元叫作一个goroutine,使用go语句创建新的goroutine(即在普通的函数或方法调用前加关键字go)。当main goroutine返回时,所有的goroutine都会被直接打断,程序退出。

例子:

func spinner(delay time.Duration) {
	for {
		for _, r := range `-\|/` {
			fmt.Printf("\r%c", r)
			time.Sleep(delay)
		}
	}
}
func fib(x int) int {
	if x < 2 {
		return x
	}
	return fib(x-1) + fib(x-2)
}

Channels

channels是goroutine之间的通信机制,一个goroutine通过channels给另一个goroutine发送值信息。

每个channel都有一个特殊的类型,即可发送数据的类型。一个可以发送int类型数据的channel一般写为chan int。

// channel对应make创建的底层数据结构的引用,零值是nil。两个相同类型的channel可以使用==运算符比较。
ch := make(chan int) // ch has type 'chan int'

channel有发送和接收两个主要操作,都使用<-运算符,发送语句中,<-分割channel和要发送的值,在接收语句中,<-放在channel对象前面。

ch<- x // 发送语句
x = <-ch // 接收语句
<-ch // 抛弃接收结果
close(ch) // 关闭channel,对已经关闭的channel进行接收操作仍然可以得到之前已经成功发送的数据,如果channel中已经没有数据的话将产生一个零值数据

无缓存的Channel

无缓存channel的发送和接收,在另一方没有准备好时会阻塞,因此其也被称为同步channel。

channel发送消息的功能:

  • 注重消息的值

  • 注重消息发生的事实和时刻,这种情况称为消息事件,如果消息事件无需携带额外信息,可以使用空结构体struct{}作为channel元素的类型,当然也可以用bool或int实现相同的功能。

    ch := make(chan struct{})
    ch <- struct{}{}
    

串联的channel(pipeline)

channel将多个goroutine链接在一起,一个channel的输出作为下一个channel的输入,这种串联的channel就是pipeline。

当关闭channel后,为了让pipeline中后续的goroutine也能停止,可以使用以下两种方式:

// pipeline中后续的goroutine
// 方式一:多接收一个bool型结果,用于表示channel是否关闭,但该方式比较繁琐
go func() {
    for {
        x, ok := <- ch
        if !ok {
            break
        }
        // work
    }
}
// 方式二:使用range循环,channel关闭后会跳出循环
go func() {
    for x := range ch {
        // work
    }
}

单向channel

因为channel具有相同的类型,所以既可以发送,又可以接收,为了防止channel滥用,Go提供单向channel类型,chan<- int表示只能发送int的channel,<-chan int表示只接收int的channel。

有缓存的channel

缓存channel内部维护一个队列,队列容量在调用make函数创建channel时通过第二个参数指定,如:

ch = make(chan string, 3) // 创建可存放3个string的缓存channel
fmt.Println(cap(ch)) // 获取缓存channel的容量
fmt.Println(len(ch)) // 获取缓存channel中当前有效元素的个数,并发过程中可能失效

发送操作就是在队列尾部插入元素,接收操作就是在队头取元素,如果队列满或空,则阻塞。

缓存channel可用于多个goroutine并发地向同一个channel发送数据,或从同一个channel接收数据:

// 并发地向三个镜像站点发出请求,接收者只接收第一个收到的响应
// 如果使用无缓存channel时,执行较慢的goroutine由于没有channel接收而永远阻塞(goroutines泄漏)
func mirroredQuery() string {
    responses := make(chan string, 3)
    go func() { responses <- request("asia.gopl.io") }()
    go func() { responses <- request("europe.gopl.io") }()
    go func() { responses <- request("americas.gopl.io") }()
    return <-responses // return the quickest response
}
func request(hostname string) (response string) { /* ... */ }

选择无缓存channel和缓存channel的建议

无缓存channel:强调发送和接收之间的同步。

缓存channel:如果未能分配足够的缓存将导致程序阻塞;在生产者和消费者之间的速率相同的情况下,能提高性能,就算不同,还可以通过增加生产者或消费者的数量来提高性能。

基于select的多路复用

IO多路复用,select会等待case中有能够执行的case时去执行。

select {
case <-ch1: // 接收结果
    // work1
case x := <-ch2: // 接收结果并给x
    // work2
case ch3<- y: // 发送
    // work3
default: // 默认情况
    // work4
}

channel的buffer大小是1,会交替的空或满:

ch := make(chan int, 1)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) // "0" "2" "4" "6" "8"
    case ch <- i:
    }
}

如果多个case同时就绪时,select会随机地选择一个执行,这样来保证每一个channel都有平等的被select的机
会。

ch := make(chan int, 3)
for i := 0; i < 10; i++ {
    select {
    case x := <-ch:
        fmt.Println(x) // 0一定会输出,但是后续的输出是不确定的
    case ch <- i:
    }
}

如果在select中,某个channel的值为nil,其永远不会被select到,因此可以利用channel是否为nil来启用或禁用case,如:

var tick <-chan Time.time
if flag {
    tick = time.Tick(500 * time.Millisecond)
}

select {
case <-tick: // 如果为nil则该case被禁用
    // work
// ...
}

并发的退出

Go语言没有提供在一个goroutine中终止另一个goroutine的方法,所以为了能够达到退出goroutine的目的,需要一个策略:不是向channel发送值,而是将关闭channel的操作进行广播。

func main() {
    // 当用户有输入时,将取消消息通过关闭done的channel广播出去
	go func() {
		os.Stdin.Read(make([]byte, 1))
		close(done)
	}()
	go work(1)
	go work(2)
	for {
		if cancelled() {
			break
		}
	}
}

var done = make(chan struct{})

func cancelled() bool {
	select {
	case <-done:
		return true
	default:
		return false
	}
}
func work(i int) {
	tick := time.Tick(500 * time.Millisecond)
	for {
		if cancelled() {
			break
		}
		fmt.Println("Hello", i)
		select {
		case <-tick:

		}
	}
}

如何确定主函数退出的时候其已经释放了所有的资源?可以调用panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,其会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间。

基于共享变量的并发

竞争条件

避免数据竞争:

  • 不要在并发过程中修改数据,或者只有某个特定的goroutine可以修改数据
  • 避免多个goroutine直接访问变量,而是通过channel来发送给指定的goroutine请求来查询更新变量。(不要使用共享数据来通信;使用通信来共享数据)
  • 允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在修改数据。

sync.Mutex互斥锁

二元信号量实现互斥锁:

var (
	sema    = make(chan struct{}, 1)
	balance int
)

func Deposit(amount int) {
	sema <- struct{}{} // 获取token
	balance += amount
	<-sema // 释放token
}
func Balance() int {
	sema <- struct{}{}
	b := balance
	<-sema
	return b
}

使用sync.Mutex作为互斥锁更简单,一般来说,被mutex所保护的变量是在mutex变量声明之后立刻声明。goroutine在结束后必须释放锁,可以通过defer调用Unlock,使得临界区隐式地延伸到函数作用域的最后,当然,性能会比显示调用Unlock低一点,毕竟作用域延伸了(可以想象Java的同步方法和同步代码块的情况)。

import "sync"
var (
    mu sync.Mutex // guards balance
    balance int
)
func Deposit(amount int) {
    mu.Lock()
    balance = balance + amount
    mu.Unlock()
}
func Balance() int {
    mu.Lock()
    defer mu.Unlock()
    return balance
}

sync.Mutex不支持重入,尝试重入会导致死锁。通过将一个函数分离为多个函数,可以将可能需要重入的情况变成无需重入的情况。

sync.RWMutex读写锁

var mu sync.RWMutex // 写锁仍然是Lock()和Unlock(),因为其是互斥锁
var balance int
func Balance() int {
	mu.RLock() // readers lock
	defer mu.RUnlock()
	return balance
}

内存同步

CPU缓存的可见性和指令重排序问题,使用互斥锁解决。

sync.Once安全的惰性初始化

惰性初始化需要一个互斥量mutex和一个bool变量来记录初始化是不是已经完成了,mutex保护bool变量和客户端数据结构,Do函数接收初始化函数作为其参数。每次调用Do都会锁定mutex,并检查bool变量。

var loadIconsOnce sync.Once
var icons map[string]image.Image
func Icon(name string) image.Image {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

竞争条件检测

Go的runtime和工具链提供了竞争检查器(the race detector),只需要在go build,go run或者go test命令后面加上-race的flag,编译器会创建一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。另外,修改版的程序会记录下所有的同步事件,比如go语句,channel操作,以及对(*sync.Mutex).Lock,(*sync.WaitGroup).Wait等等的调用。

Goroutines和线程

  • 传统的线程栈是固定的大小2MB,大部分情况下对于goroutines来说太大,是内存的浪费,但是对于深层次的递归却因太小而受到限制;goroutines使用动态栈,作用和线程的栈相同,但是goroutines的栈一般只需要2KB,且最大可以调整到1GB。
  • 线程的调度由操作系统进行,需要保护现场和恢复线程;Go使用自己的调度器,其使用了一些技术手段,比如m:n调度会在n个操作系统线程上多工(调度)m个goroutine。
  • Go的调度器使用了一个叫做GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码,默认值为CPU核心数。在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。可用GOMAXPROCS环境变量显式控制参数。
  • 线程有id号,因此可以使用线程本地存储;goroutine没有id号,这是为了防止线程本地存储的滥用而有意为之的。

包和工具

Go语言编译快的三个原因:1)所有导入的包在文件的开头显式声明,编译器无需判断包的依赖关系;2)禁止包的循环依赖,依赖关系为有向无环图,可以并发编译;3)编译后包的目标文件记录包本身的导出信息,也记录包的依赖关系。

包的匿名导入作用:实现编译时机制,导入附加的包供其他包使用,而不是在本文件的main方法中使用,比如导入数据库驱动:

import (
	"database/sql"
    _ "github.com/lib/pq" // enable support for Postgres
    _ "github.com/go-sql-driver/mysql" // enable support for MySQL
)
db, err = sql.Open("postgres", dbname) // OK
db, err = sql.Open("mysql", dbname) // OK
db, err = sql.Open("sqlite3", dbname) // returns error: unknown driver "sqlite3"

包名一般采用单数的形式。标准库的bytes、errors和strings使用了复数形式,这是为了避免和预定义的类型冲
突,同样还有go/types是为了避免和type关键字冲突。

内部包:包封装机制的中间状态,对于一小部分信任的包是可见的,但并不是对所有的包都是可见的,使用internal包,Go对包含internal名字的路径段导入路径做了特殊处理,其只能被和internal目录有同一个父目录的包所导入,如:

// net/http/internal/chunked内部包能被net/http/httputil或net/http包导入,但是不能被net/url包导入
net/http
net/http/internal/chunked
net/http/httputil
net/url

工具

GOPATH环境变量指定当前工作目录,当需要切换工作区时,更新该环境变量即可。

GOROOT环境变量指定Go的安装目录,以及自带的标准库包的位置。

常用的命令工具

go get [-u] 包名 // 下载包。-u表示将确保所有的包和依赖包都是最新的,然后重新编译安装它们
go build // 编译其后面指定的每个包。如果包是一个库,则忽略输出结果;如果包名是main,则其调用连接器在当前目录创建一个可执行程序。默认指定为当前目录下的包,也可以用相对路径(必须以.或者..开头)
go run // 结合编译和运行过程
go install // 编译包。go install保存每个包的中间编译结果,go build丢弃除了最后的可执行文件之外的所有中间编译结果。两者都不会重新编译没有发生变化的包,go build -i等价于go install
go doc // 打印包的声明和每个成员的文档注释,不区分大小写。 go doc time, go doc time.Since 某个具体包成员的注释, go doc time.Durations.Second 某个具体包的一个方法的注释
godoc // 提供交叉引用的HTML页面,包含比go doc更多的信息
go list // 查询可用包的信息。使用 ... 参数表示所有包,也可以指定特定目录下的所有包,或者某个包。go list ...xml... 列出与xml相关的所有包。go list -json hash 以JSON格式展示包的元信息。
// -f参数允许用户使用text/template包的模板语言定义输出文本的格式,如 go list -f '{{join .Deps " "}}' strconv 表示strconv包的依赖包以空格隔开显示

build注释控制:

  • 只有在编译程序对应的目标操作系统是Linux或Mac OSX时才编译该文件

    // +build linux darwin
    
  • 不编译文件

    // +build ignore
    
  • 更多细节可参考文档,go doc go/build

测试

在包目录中以_test.go为后缀的文件不是go build构建包时的一部分,而是go test测试的一部分,包含三种类型的函数:

  • 测试函数,以Test为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确
  • 基准测试函数,以Benchmark为函数名前缀的函数,它们用于衡量一些函数的性能
  • 示例函数,以Example为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。

测试函数

go test命令会遍历所有的_test.go文件中符合上述命名规则的函数,然后生成一个临时的main包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。

// t参数用于报告测试失败和附加的日志信息
func TestName(t *testing.T) {
    // ...
}

func TestPrime(t *testing.T) {
    if !IsPrime(5) {
        t.Error(`IsPrime(5) = false`)
    }
}
func TestNonPrime(t *testing.T) {
    if !IsPrime(6) {
        t.Error(`IsPrime(6) = true`)
    }
}

go test -v // -v参数可以打印每个测试函数的名字和运行时间
go test -run="hello|world" // -run参数对应一个正则表达式,只有测试函数名被它正确匹配的测试函数才会被测试
// 也可以使用测试表格

基准测试

func Benchmark10(b *testing.B) { benchmark(b, 10) }
func Benchmark100(b *testing.B) { benchmark(b, 100) }
func Benchmark1000(b *testing.B) { benchmark(b, 1000) }

func benchmark(b *testing.B, size int) { 
	for i := 0; i < size; i++ {
    	IsPrime(5)
    }
}

默认情况下不会进行基准测试,使用-bench参数指定要运行哪些基准测试函数,.匹配所有的基准测试函数;-benchmem 参数报告包含内存的分配数据统计。

测试分析

Go的测试工具提供了几种分析方式:

go test -cpuprofile=cpu.out // CPU分析
go test -blockprofile=block.out // 阻塞分析
go test -memprofile=mem.out // 堆分析

// 分析样例
go test -run=NONE -bench=ClientServerParallelTLS64 -cpuprofile=cpu.log net/http // 基准测试默认包含单元测试,用-run=NONE参数禁止单元测试
go tool pprof -text -nodecount=10 ./http.test cpu.log // -text指定输出格式,在这里每行是一个函数,根据使用CPU的时间长短来排序。-nodecount=10限制只输出前10行结果,足够查明严重的性能问题。

示例函数

func ExampleIsPrime() {
    fmt.Println(IsPrime(5))
    fmt.Println(IsPrime(6))
    // Output:
    // true
    // false
}

示例函数没有函数参数和返回值。作用有:

  • 作为文档。展示函数的用法,也可以用于展示属于同一个接口的几种类型或函数直接的关系。示例函数和注释的区别在于示例函数需要真正地运行。
  • 如果示例函数内含有类似上面例子中的// Output:格式的注释,那么测试工具会执行这个示例函数,然后检测这个示例函数的标准输出和注释是否匹配。
  • 提供一个真实的演练场,如http://golang.org

反射

反射机制是在运行时更新变量和检查它们的值、调用它们的方法和它们支持的内在操作,使得可以将类型本身作为第一类的值类型处理。

需要反射的原因:

  • 一个函数要处理不满足普通公共接口的类型的值。
  • 类型没有确定的表示方式。
  • 设计函数时,类型可能还不存在。如fmt包提供的字符串格式化。

reflect.Type和reflect.Value

Type表示Go类型,它是一个接口。函数reflect.TypeOf接受任意的interface{}类型,并返回对应动态类型的reflect.Type(具体的动态类型):

t := reflect.TypeOf(3) // 3作为 interface{} 类型参数,将一个具体的值转换为接口类型会有一个隐式的接口转换操作,包含操作数的动态类型(int)和动态值(3)
fmt.Println(t) // "int"
fmt.Printf("%T\n", 3) // "int"
fmt.Println(t.String()) // "int"

Value可以持有一个任意类型的值。函数reflect.ValueOf接受任意的interface{}类型,并返回对应动态类型的reflect.Value:

v := reflect.ValueOf(3)
fmt.Println(v) // "3"
fmt.Printf("%v\n", v) // "3"
fmt.Println(v.String()) // NOTE: "<int Value>"

t := v.Type() // reflect.Type
x := v.Interface() // interface{}
i := x.(Int) // int类型,为3

reflect.Value和interface{}都能保存任意的值,区别是空接口隐藏了值对应的表示方式和所有的公开方法,因此只有知道具体的动态类型才能使用类型断言来访问内部的值,而Value有很多方法可以检查其内容。

Value的Kind方法会返回Value的类型,包括1)Bool、String和数字这三种基础类型;2)Array和Struct的聚合类型;3)Chan、Func、Ptr、Slice、Map的引用类型;4)接口类型;5)表示空值的无效类型。

switch v.Kind() {
case reflect.Invalid: // 表示空值的无效类型
    return "invalid"
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
    return strconv.FormatInt(v.Int(), 10)
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
    return strconv.FormatUint(v.Uint(), 10)
    // ...floating-point and complex cases omitted for brevity...
case reflect.Bool:
    return strconv.FormatBool(v.Bool())
case reflect.String:
    return strconv.Quote(v.String())
case reflect.Chan, reflect.Func, reflect.Ptr, reflect.Slice, reflect.Map:
    return v.Type().String() + " 0x" + strconv.FormatUint(uint64(v.Pointer()), 16)
default: // reflect.Array, reflect.Struct, reflect.Interface
    return v.Type().String() + " value"
}

对于不同的类型,Value的不同方法为:

  • Slice和数组:Len方法返回元素个数,Index(i)返回下标对应的元素(Value类型)。
  • 结构体:NumField方法返回结构体中成员的数量,Field(i)返回第i个成员的值(Value类型)。
  • Map:MapKeys方法返回Slice(Value类型),MapIndex(key)返回key对应的值(Value类型)。
  • 指针:Elem方法返回指针指向的变量,IsNil方法来显式地测试一个空指针(nil)。
  • 接口:Elem方法返回接口对应的动态值,IsNil方法来测试接口是否是nil。

通过reflect.Value修改值

变量是一个可寻址的内存空间,可以通过内存地址更新变量,然而不是变量就无法完成更新(如x + 1),类似于这样的情况,reflect.Value有些是可取地址的(可更新),也有些是不可取地址的。

x := 2 						// value	type 	variable?
a := reflect.ValueOf(2) 	// 2		int 	no		 a是整数2的拷贝副本
b := reflect.ValueOf(x) 	// 2		int 	no		 b也是整数2的拷贝副本
c := reflect.ValueOf(&x) 	// &x 		*int 	no		 c是指针&x的拷贝
d := c.Elem() 				// 2 		int 	yes (x)  d是c的解引用生成的,指向一个变量,是可取地址的
// CanAddr方法用来判断是否是可取地址
fmt.Println(a.CanAddr()) // "false"
fmt.Println(b.CanAddr()) // "false"
fmt.Println(c.CanAddr()) // "false"
fmt.Println(d.CanAddr()) // "true"

通过指针间接地获取的reflect.Value都是可取地址的,如Index方法、Elem方法,修改值的方式如下:

// 使用指针的方式
x := 2
d := reflect.ValueOf(&x).Elem()
px := d.Addr().Interface().(*int)
*px = 4
fmt.Println(x)

// 使用Set方法
d.Set(reflect.ValueOf(5))
fmt.Println(x)
// Set有很多基本数据类型的方法:SetInt、SetUint、SetString和SetFloat等
d.SetInt(10)
fmt.Println(x)
// CanSet方法用于检查Value是否是可取地址且可被修改的,而CanAddr方法不能判断一个变量能否被修改

反射的问题

反射不应过度使用:

  • 基于反射的代码比较脆弱。错误要在运行时才会抛出;如果暴露反射API可能会被攻击;影响自动化重构和分析工具的准确性,因为它们无法识别运行时才能确认的类型信息。
  • 反射的操作不能做静态类型检查,且反射代码难以理解。
  • 反射的运行速度比正常的代码慢一到两个数量级。

底层编程

unsafe包由编译器实现,被广泛地用于比较低级的包,如runtime、os、syscall、net包等。使用unsafe包的同时也放弃了与未来版本的兼容。

unsafe.Sizeof, Alignof 和Offsetof

Sizeof函数返回操作数在内存中的字节大小(uintptr类型),参数可以是任意类型的表达式,但是它并不会对表达式进行求值。其返回的大小只包括数据结构中固定的部分,如字符串对应结构体中的指针和字符串长度部分,但不包括指针指向的字符串内容。

计算机在加载和保存数据时,如果内存地址合理地对齐将会更有效率。内存空洞是编译器自动添加的没有被使用的内存空间,用于保证后面每个字段或元素的地址相对于结构或数组的开始地址能够合理地对齐(内存空洞可能会存在一些随机数据,可能会对用unsafe包直接操作内存的处理产生影响)。如果编译器没有重新排列每个字段的内存位置,那么不同的顺序会导致不同的内存空间大小:

							   // 64-bit	32-bit
struct{ bool; float64; int16 } // 3 words	4words,该写法比下面两种写法多50%内存
struct{ float64; int16; bool } // 2 words	3words
struct{ bool; int16; float64 } // 2 words	3words

Alignof函数返回对应参数的类型需要对齐的倍数。

Offsetof函数的参数必须是一个字段,如x.f返回f字段相对于x起始位置的偏移量:

var x struct {
    a bool
    b int16
    c []int
}
// 64位系统
Sizeof(x)   = 32	Alignof(x) = 8
Sizeof(x.a) = 1		Alignof(x.a) = 1 	Offsetof(x.a) = 0
Sizeof(x.b) = 2		Alignof(x.b) = 2 	Offsetof(x.b) = 2
Sizeof(x.c) = 24	Alignof(x.c) = 8 	Offsetof(x.c) = 8

unsafe.Pointer

nsafe.Pointer是特别定义的一种指针类型,它可以包含任意类型变量的地址。

var x struct {
    a bool
    b int16
    c []int
}
// 和pb := &x.b 等价
pb := (*int16)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)))
*pb = 42
fmt.Println(x.b) // "42"
pa := (*bool)(unsafe.Pointer(uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.a)))
*pa = true
fmt.Println(x.a) // true

// 不正确的代码。
// 原因是垃圾回收器会移动一些变量以降低内存碎片,这称为移动GC。当变量被移动,所有的保存该变量旧地址的指针必须同时被更新为变量移动后的新地址。unsafe.Pointer是一个指向变量的指针,因此当变量被移动时对应的指针也必须被更新,而uintptr类型的临时变量只是一个普通的数字,所以其值不会被改变。
tmp := uintptr(unsafe.Pointer(&x)) + unsafe.Offsetof(x.b)
pb := (*int16)(unsafe.Pointer(tmp))
posted @ 2023-04-05 21:11  sjmuvx  阅读(116)  评论(0编辑  收藏  举报