更多:go官网 
 
题解:https://github.com/shenlvmeng/go-learning-exercise 
 
历史 Go语言构想与2007年9月,于2009年11月发布。主要思想来自3种语言:
C,基础语法和编译 
Pascal,包概念 
CSP(Communication Sequential Process),并发思想 
 
Go项目诞生是为了解决Google中系统复杂性太高的问题。因此,简单性是Go思想的重要部分。设计上,Go
没有隐式类型转换 
没有构造和析构函数 
没有运算符重载 
没有形参默认值 
没有继承 
没有泛型 
没有异常 
没有宏(macro) 
没有函数注记 
没有线程局部存储 
 
快速开始 范例1:Hello world 1 2 3 4 5 6 7 package  mainimport  "fmt" func  main ()     fmt.Println("Hello world!" ) } 
在诸多语言中,C对Go的影响是最深的。.go文件需要经过编译成二进制文件才可以运行。
go run可以直接运行.go文件go build可以编译生成二进制文件,并在之后直接执行 
在代码结构上,
先声明当前包名,其中命名为main的包名代表代码是可执行程序,而非一个库文件 
再import依赖包,go自带100+内置包。在编译时,编译器会抛弃未被使用的包,减少体积 
接下来是程序代码,命名为main的函数是执行的入口 
 
Go代码有着标准的代码格式,并可以通过gofmt格式化代码。代码中不需要在行尾写分号 ,后面紧跟特定token的换行符会自动转成分号。因此,Go代码中换行会影响代码编译。
范例2:命令行参数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package  mainimport  (    "fmt"      "os"  ) func  main ()     var  s, sep string      for  i:= 1 ; i < len (os.Args); i++ {         s += setp + os.Args[i]         setp = " "      }     fmt.Println(s) } 
切片(slice)是序列数组元素的表示方式,可以用s[i]或s[m:n](m或n缺失是表示头和尾元素位置)获取1或n-m个元素。使用len(s)获取长度。 
注释以//开头 
import多个库时,可以用()包裹列表的形式声明,这种写法更为常见 
使用var开头表示变量声明,未指定初始值的变量会隐式初始化为当前类型的“零值”(0或’’等) 
:=式的声明可以省去var更快地为一组变量初始化go中的for循环是唯一的循环语句 ,分为以下三部分。缺失initialization和condition时可以表示while循环 
 
1 2 3 for  initialization; condition; post {     } 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  mainimport  (    "fmt"      "os"  ) func  main ()     s, sep := "" , ""      for  _, arg := os.Args[1 :] {         s += sep + arg         sep = " "      }     fmt.Println(s) } 
在循环中,range可以生产一对值,index和value 
_专门用来替代不需要使用的变量名,否则go会报错另外也可以直接用strings.Join方法实现效果 
 
范例3:寻找重复行 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  mainimport  (    "fmt"      "bufio"      "os"  ) func  main ()     counts = make (map [string ]int )     input = bufio.NewScanner(os.Stdin)     for  input.Scan() {         counts[input.Text()]++     }     for  line, n := range  counts {         if  n > 1  {             fmt.Printf("%d\t%s\n" , n, line)         }     } } 
同for循环一样,if语句也不需要()包裹 
内置的make函数可以创建一个新的map。map也可以被for循环遍历,每次循环的pair分别是key和value 
counts[input.Text()]中当key不存在时,会返回零值0bufio库可以更方便地帮忙处理程序的输入(input)和输出(output)
input.Scan()获取下一行,并自动去掉末尾换行符,在没有内容时返回falseinput.Text()获取当前位置的文本 
 
Printf和C语言风格类似,里面行如%s,%v的特殊符号称为verbs  
从文件中寻找代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 func  main ()     counts := make (map [string ]int )     files := os.Args[1 :]     if  len (files) == 0  {         countlines(os.Stdin, counts)     } else  {         for  _, file := range  files {             f, err := os.Open(file)             if  err != nil  {                 fmt.Fprintf(os.Stderr, "dup: %v\n" , err)                 continue              }             countLines(f, counts)             f.Close()         }     }     for  line, n := range  counts {         if  n > 1  {             fmt.Printf("%d\t%s\n" , n, lines)         }     } } func  countlines (f *os.File, counts map [string ]int )     input := bufio.NewScanner(f)     for  input.Scan() {         counts[input.Text()]++     }      } 
除了上面的流模式读取文件外,还可以直接把整个文件直接读进内存,再将二进制数据string化并处理。此处可以使用io/ioutil中的ReadFile方法。转换过程用string(data)完成。
日常使用时,通常借助bufio,ioutil等高层级API就可以完成任务,而不需要深入实现内部。
范例4:Gif 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package  mainimport  (	"image"  	"image/color"  	"image/gif"  	"io"  	"math"  	"math/rand"  	"os"  ) var  palette = []color.Color{color.White, color.Black}const  (	whiteIndex = 0  	blackIndex = 1  ) func  main () 	lissajous(os.Stdout) } func  lissajous (out io.Writer) 	const  ( 		cycles  = 5  		res     = 0.001  		size    = 100  		nframes = 64  		delay   = 8  	) 	freq := rand.Float64() * 3.0  	anim := gif.GIF{LoopCount: nframes} 	phase := 0.0  	for  i := 0 ; i < nframes; i++ { 		rect := image.Rect(0 , 0 , 2 *size+1 , 2 *size+1 ) 		img := image.NewPaletted(rect, palette) 		for  t := 0.0 ; t < cycles*2 *math.Pi; t += res { 			x := math.Sin(t) 			y := math.Sin(t*freq + phase) 			img.SetColorIndex(size+int (x*size+0.5 ), size+int (y*size+0.5 ), blackIndex) 		} 		phase += 0.1  		anim.Delay = append (anim.Delay, delay) 		anim.Image = append (anim.Image, img) 	} 	gif.EncodeAll(out, &anim) } 
使用const声明常量,常量的值只能是number,string或boolean 
gif.GIF{...}是合成字面量的写法,其类型是struct,可以字面量声明其field,未声明fields均为零值(zero value)image库API可以操作图像 
 
范例5:fetch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package  mainimport  (	"fmt"  	"io"  	"net/http"  	"os"  	"strings"  ) const  prefix = "http://" func  main () 	for  _, url := range  os.Args[1 :] { 		if  !strings.HasPrefix(url, prefix) { 			url = prefix + url 		} 		resp, err := http.Get(url) 		if  err != nil  { 			fmt.Fprintf(os.Stderr, "fetch: %v\n" , err) 			os.Exit(1 ) 		} 		_, err = io.Copy(os.Stdout, resp.Body) 		resp.Body.Close() 		if  err != nil  { 			fmt.Fprintf(os.Stderr, "fetch: reading %s: %v\n" , url, err) 		} 	} } 
和网络相关的API都位于net库中,如http.Get(url) 
os.Exit(1)代表异常退出 
范例6:并行fetch 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package  mainimport  (	"fmt"  	"io"  	"io/ioutil"  	"net/http"  	"os"  	"time"  ) func  main () 	start := time.Now() 	ch := make (chan  string ) 	for  _, url := range  os.Args[1 :] { 		go  fetch(url, ch) 	} 	for  range  os.Args[1 :] { 		fmt.Println(<-ch) 	} 	fmt.Printf("%.2fs elapse\n" , time.Since(start).Seconds()) } func  fetch (url string , ch chan <- string ) 	start := time.Now() 	res, err := http.Get(url) 	if  err != nil  { 		ch <- fmt.Sprint(err) 		return  	} 	nbytes, err := io.Copy(ioutil.Discard, res.Body) 	res.Body.Close() 	if  err != nil  { 		ch <- fmt.Sprintf("while reading %s: %v" , url, err) 		return  	} 	secs := time.Since(start).Seconds() 	ch <- fmt.Sprintf("%.2fs %7d %s" , secs, nbytes, url) } 
goroutine是go中并行执行函数的表示,channel是goroutine间相互沟通的方式,传递特定类型数据。goroutine相互沟通时,沟通的两者会对其他goroutine block ,保证没有冲突
goroutine使用go创建,channel使用chan创建,ch <-表示向channel发送,<- ch表示从channel接收 
 
 
ioutil.Discard输出流会直接丢弃流内容 
范例7:web server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package  mainimport  (	"fmt"  	"log"  	"net/http"  ) func  main () 	http.HandleFunc("/" , handler) 	log.Fatal(http.ListenAndServe("localhost:8000" , nil )) } func  handler (w http.ResponseWriter, r *http.Request) 	fmt.Fprintf(w, "Path = %q\n" , r.URL.Path) } 
使用http库的HandleFunc和ListenAndServer可以便捷地启动一个服务器 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package  mainimport  (	"fmt"  	"log"  	"net/http"  	"sync"  ) var  mu sync.Mutexvar  count int func  main () 	http.HandleFunc("/" , handle) 	http.HandleFunc("/count" , counter) 	log.Fatal(http.ListenAndServe("localhost:8000" , nil )) } func  handle (w http.ResponseWriter, r *http.Request) 	mu.Lock() 	count++ 	mu.Unlock() 	fmt.Fprintf(w, "path = %q\n" , r.URL.Path) } func  counter (w http.ResponseWriter, r *http.Request) 	mu.Lock() 	fmt.Fprintf(w, "count %d\n" , count) 	mu.Unlock() } 
server会为每个请求创建一个新的goroutine处理,为了避免并发读写count时的bug,使用了mutex锁保证读写是都是串行的 
os.Stdout,ioutil.Discard, http.ResponseWriter都实现了io.Writer接口,因此可以用在任何需要输出流的地方if语句前可以增加前置语句,如if err:=xxx; err != nil 
 
杂项 
go中的switch没有fall through机制,若需要,需要显示声明fallthrough。case支持表达式,switch后支持没有操作数,此时的switch称为tagless switch。break, continue, goto命令如常 
go中有命名类型,类似ts中的interface,行如type Point struct { X, Y int } 
go中有指针,*表示去指针对应的值,&表示取变量的指针,另外不支持指针上的算术运算  
go中的方法指命名类型上的函数,interface意义如常 
可以去这里 寻找标准库的包,或去这里 寻找社区贡献的包 
注释风格同其他语言,//表示单行注释,/* */表示多行注释。不支持嵌套注释 
 
程序结构 命名 和JS类似
以**Unicode字母或下划线_**开头 
后跟Unicode字母或数字或下划线  
大小写敏感  
go目前(2020/01/01)有25个不允许用来命名的关键字 ,其中几个可能是对前端较难想到的是
selectdeferchanrangefallthrough 
另外,还有一些预定义常量、类型、函数可以用来命名 ,但很容易造成误解,下面举些例子:
常量:true iota nil 
类型:int complex128 uintptr rune error 
函数:make len imag panic close 
 
包名始终小写 ,在函数域内命名的函数只在函数域内可见,否则在整个包域内可见 。整个包内声明的变量用首字母区分可见性:
首字母大写的可以被其他包访问 ,如fmt.FPrintf反之则只在包内可见 
 
命名长度没有限制,但建议scope越大的变量命名越长 。Go使用驼峰风格的变量命名,首字母缩略词和首字母同大写或同小写 。
声明 声明有4钟:
var 变量const 常量type 类型func 函数 
声明在函数域内可见,或在整个包域内可见。函数返回可以是一组变量。
变量 1 var  name type  = expression
Go中的变量声明如上所示,其中的type部分或expression部分可以省略,但是不能同时省略 。
type缺失时,name的类型由expression字面量或返回值决定 
expression缺失时,name的值自动设置为type类型的“零值”(zero value) 
数值零值为0,字符串零值为"",布尔类型零值为false 
其余接口或引用类型零值为nil,如指针、map、切片、函数、channel 
聚合类型的零值即其所有组成元素的零值 
 
 
 
所以,Go中不存在未初始化的变量 。包级别变量在main函数开始前初始化,局部变量在声明过程中初始化。一组变量可以同时被初始化。
1 2 var  b, f, s = true , 1.3 , "string" var  f, err = os.Open(name)
简写式 在函数域 内的局部变量声明 可以使用简写式,即:=。在已知变量初始值时可以省去写var。在初始值并不重要或最好显式写明类型时,还是使用var foo type的形式比较好。和var声明一样,也可以同时用简写式声明多个局部变量。但要注意,不要把这种写法和元组赋值(tuple assignment) 搞混了。
1 2 3 4 5 i, j := 1 , true  i, j = j, i 
另外,简写式声明里可以写部分 已经声明的局部变量,在这里会当做赋值处理。但是简写式声明中要至少包含一个未声明变量 
1 2 in, err := os.OpenFile(infile) out, err := os.OpenFile(outfile) 
指针 Go中的指针和C中类似,用&表示取一个变量 的地址,用*表示访问某个地址所在的位置。指针的零值为nil,因此可以用p != nil来判断指针是否指向变量。
new函数可以通过new函数,声明类型T创建新的匿名 变量,函数返回变量的指针即*T类型。这在不需要变量名时很好用。每次调用new函数新建变量时,返回的地址不同 ,除非类型不附加任何信息 ,如struct {}或[0]int。
另外,由于new只是预定义函数,所以可以用来做变量名。
生命周期 生命周期即变量从创建到被回收的时间。包级别的变量会在整个程序执行过程中存在。局部变量则会在未被引用(unreachable) 时释放内存。Go中的垃圾回收机制会自动帮你完成这件事。但是如果有下面这种情况出现,则会阻止垃圾回收释放内存。
1 2 3 4 5 6 var  global *int func  f () 	var  x int  	x = 1  	global = &x } 
在上述情况下,x局部变量从f函数中逃逸 ,并不会在f函数返回时被回收,持久存储在堆(heap)中。应尽量避免这种情况带来的额外内存损耗。
赋值 和其余语言赋值没什么太大区别。
不同的是,额外增加了元组赋值 。=右侧的一组变量会先求值,再赋给左侧变量。建议在不需要复杂运算时使用。同时,有些表达式和函数也会返回一组值,此时需要用元组赋值的方式接收。在不需要某个变量时,可以使用_占位。
1 2 3 4 5 6 x, y= y, x f, err = os.Open("foo.txt" ) v, ok = m[key] v, ok = x.(T) v, ok = <- ch 
可赋值性 除了一些显式的赋值外,还有函数返回、字面量声明等。Go中的赋值当且仅当 =左右的值和变量类型相同 才可进行(对于==和!=的判断也是这样)。nil可以赋值给任何复杂类型或引用类型。
类型声明 Go中可以定义类型。Go中的类型定义储存值的符号、它们的大小、固有操作以及方法,使用type name underlying-name声明。它通常出现在包级别,有些也会通过首字母大写的形式export出去。
1 2 3 4 5 6 7 8 9 type  Celsius float64 type  Fahrenheit float64 func  CTOF (c Celsius) 	return  Fahrenheit(c * 9  / 5  + 32 ) } func  FTOC (f Fahrenheit) 	return  Celsius((f - 32 ) * 5  / 9 ) } 
两个有着相同底层类型的命名类型并不是同一种类型 ,也不能直接相互赋值和比较。但是可以使用强制类型转换 转换到想同类型来比较。所有的类型T都有对应的强制类型转换操作T(x)。两个有相同类层类型或指向相同底层类型的未命名指针可以相互强制转换。另外,Go中的强制类型转换从不会在运行时出错。
比较特别的是,类型上还可以声明方法。
1 2 3 func  (c Celsius) string  {	return  fmt.Sprintf("%g°C" , c) } 
包和文件 Go中的包即其他语言中的库、模块。以实现模块化、封装、分发和重用。和Java类似,一个包的代码可以存放在多个文件内 ,通常位于同一个文件夹下。每个包都有相互隔离的命名空间,需要用·image.Decode的形式使用。需要export的变量、类型、函数使用首字母大写 的形式。
建议在每个export出去的变量、类型、函数前使用注释说明。另外,建议在包开头留下doc comment ,或将更多注释放在doc.go中。
import 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  mainimport  (	"fmt"  	"os"  	"strconv"  	"shenlvmeng/learning/tempconv"  ) func  main () 	for  _, arg := range  os.Args[1 :] { 		t, err := strconv.ParseFloat(arg, 64 ) 		if  err != nil  { 			fmt.Fprintf(os.Strerr, "convert error: %v\n" , err) 			os.Exit(1 ) 		} 		f := temconv.Fahrenheit(t) 		c := tempconv.Celsius(c) 		fmt.Printf("%s = %s, %s = %s\n" , f, tempconv.FTOC(f), c, tempconv.CTOF(c)) 	} } 
每一个包都有自己的import路径,Go语言标准并不定义如何解释import路径,这一步交给解释工具完成。每个包的包名通常和路径的最后一段同名。为避免包名冲突,import可以指定包的别名。
在引用了未被使用的包时会报错。,借助goimports等工具和正确的IDE配置,可以在保存代码时自动标准化代码格式。
包初始化 1 2 3 var  a = b + c var  b = f() var  c = 1  
初始化时,先按照依赖的顺序初始化包级别变量。而.go文件的处理顺序则按照传给go编译器的顺序。初始化的过程是自底向上的,即当所有依赖包都初始化完成后,才会初始化main包,再执行main函数。对于初始化过程复杂的变量,可以在init函数中声明,而init是在程序启动时,按照声明的顺序一个一个执行的。
作用域 作用域是编译时的,和运行时的生命周期概念相对应。作用域描述一个声明的可见范围。和C系列语言类似,用大括号{}包裹会形成词法块作用域 。Go在全局作用域 下预定义了一些常量、函数、类型等。在函数外声明的作用域是包级别的,import进来的包作用域是文件级别的。局部声明只在块作用域内。内部作用域会覆盖外部作用域的同名声明。
另外,Go中还有一些隐式 的作用域,比如for,if,switch表达式中的作用域。
1 2 3 4 5 6 7 8 9 10 func  main () 	x := "Hello"  	for  i:= 0 ; i < len (x); i++ { 		x := x[i] 		if  x != "o"  { 			x := x + 'A'  - 'a'   			fmt.Prinf("%c" , x) 		} 	} } 
上面的for和if内部的x是一个单独的作用域。另外注意,简写式中会声明局部变量,会覆盖外部的同名变量,可能会带来意料之外的结果。可以通过var xxx type的形式声明变量。
1 2 3 4 5 6 7 var  cwd string func  main () 	cwd, err := os.Getwd()  	if  err != nil  { 		log.Fatalf("os.Getwd failed: %v" , err) 	} } 
基础数据结构 Go有4大类数据类型:
基础类型 ,即数字、字符串、布尔值聚合类型 ,即数组、struct引用类型 ,包括函数、指针、slice、map、channel接口类型  
这一部分先说基础类型
整型 Go的数字类型包含了不同size的整型、浮点数和复数,以及它们的有无符号性。
整型有8、16、32、64四种长度,以及对应的signed和unsigned。组合一下即下面8种:
int8int16int32int64uint8uint16uint32uint64 
另外,rune是int32的别称,通常用来表示1个Unicode字符,byte是unit8的别称。uintptr用来表示一个可以承载任意指针值的无符号整型。
Go中对整型的处理和C风格很像。
用首位表示符号位(signed int中) 
类型承载范围和C一样,如int8表示-128到127 
和C一样的操作符以及优先级,唯一区别是&^表示位清除,x &^ y表示根据y各位将x各位清0 
<<左移位,空位取0,>>右移位,无符号数补零,有符号数补符号位 整型间除法会清除小数部分 
取余%运算结果符号和被除数有关 
超过位数的会溢出 
0开头表示八进制(通常用来表示POSIX系统中文件权限设置),0x表示十六进制 
不一样的是:
相同类型才可比较大小,否则需要用int()强制转换为1种类型再比较。某些类型转换只改变值的使用方式,有些则会改变值,如int和float之间的转换 
用%d, %o, %x分别表示整型、八进制和十六进制数 
用%c表示显示对应的Unicode字符,%q显示带引号版本 
 
浮点数 浮点数有float32和float64两种类型,服从IEEE754标准。为保证精确性,通常使用flaot64。另外,还有以下特点
.前后的0可以省略%g, %e和%f分别打印最合适展示精确度版本、指数版本和原始版本有+Inf, -Inf和NaN特殊值,表现类似JS 
 
复数 Go有两种复数类型:complex64和complex128,它们其实是由float32和float64组成的。复数可以通过complex内置函数初始化,或者直接使用字面量。
1 2 var  x complex128  = complex (1 , 2 ) y := 3  - 4i  
复数间可以判断相等性,无法判断大小。math/cmplx包里包含一些复数的数学运算。
布尔类型 即type bool,和其他语言类似,有短路行为,&&比||优先级更高。bool类型和整型之间不能相互隐式转换。
字符串 字符串表示一组不可修改的比特位序列 ,但通常用来承载可读的Unicode编码点。len返回字符串长度,[i]返回第i个字节 处的值。越界读取会导致panic。
s[i:j]表示自带的substring操作,其中i和j均可省略字符串间可以比较大小和相等性,其中大小通过逐字节子母序比较 
+可表示字符串拼接不可变性:不允许修改字符串的值(如s[0] = 'L'),这使得Go可以在底层复用字符串,节省内存 
 
字面量字符串 用双引号"包裹,UTF-8编码。双引号中的反斜线\有特殊含义。如
\n表示换行\t表示制表符\x表示后接十六进制的高低位\ooo表示三个八进制位 
由反引号`` `包裹的表示纯文本字面量,其中的换行和格式也会被跨平台保留。可以用来书写多行字符串。
Unicode表示 Go中使用UTF-8变长编码:
0xxxxxxx表示ASCII码11xxxxxx 10xxxxxx 表示两字节长度110xxxxx 10xxxxxx 10xxxxxx表示三字节长度1110xxxx 10xxxxxx 10xxxxxx 10xxxxxx 表示四字节长度 
可以由\uhhhh表示16比特或\U表示32比特,如世界:\u4e16\u754c。unicode包和unicode/utf8包提供了编解码工具。utf6.DecodeRuneInString可以读取一个自然字符的数据,而非一个字节一个字节读取,utf8.RuneCountInString返回字符串的自然字符长度。幸运的是,range循环会自动调用utf8解码其中的自然字符。
1 2 3 4 5 import  "unicode/uft8" s := "Hello, 世界"  fmt.Println(len (s))  fmt.Println(utf8.RuneCountInString(s)  
当Go Unicode解析失败时,会使用特殊的Unicode占位符\ufffd,显示为带有问号的特殊字符。另外,rune[]可以直接将字符串转成编码后的每个Unicode编码点。这个rune数组进行string()强制类型转换后即原始字符串。当然你也可以直接string()装换一个整型数,不合规的整形数会得到上面提到的特殊字符。
1 2 3 4 5 6 s := "世界"  r := []rune (s) fmt.Println(string (r)) fmt.Println(string (65 )) fmt.Printlf(string (12341234 )) 
字符串和Byte Slices bytes, strings, strconv, unicode是和string相关的几个包。strings提供基本的字符串搜索、比较、修改等操作,bytes提供修改字节数组的一些操作。有时,使用byte.Buffer类型,在操作字符串字节时会更有效率。strconv提供了将其他类型转成字符串和修饰字符串的操作函数。unicode提供了一些以rune为中心的函数,如IsDigit, IsLetter, isUpper等。
1 2 3 4 5 6 7 8 9 func  basename (s string ) string  {	slash := strings.LastIndex(s, "/" ) 	s = s[slash+1 :] 	if  dot := strings.LastIndex(s, "." ); dot >= 0  { 		s = s[:dot] 	} 	return  s } 
path和path/filepath包提供了更多文件夹和目录的操作函数。
尽管字符串中的字节序列是不可更改的。其对应的字节序列数组则是可以自由修改的 。[]byte(s)会分配一个字符串s的字节序列拷贝,也可以对应用string(b)还原。bytes包提供的Buffer类型可以很方便地承载[]byte类型。
1 2 3 4 5 6 7 8 9 10 11 12 func  intsToString (values []int ) string  {	var  buf bytes.Buffer 	buf.WriteByte('[' ) 	for  i, v := range  values { 		if  i > 0  { 			buf.WriteString(", " ) 		} 		fmt.Fprintf(&buf, "%d" , v) 	} 	buf.WriteByte(']' ) 	return  buf.String() } 
上述函数中,WriteString和WriteByte用于向Buffer中写入字节或字节序列,该类型还有许多其他应用场景。
字符串和整型间的转换 
字符串 -> 整型 ,fmt.Sprintf或strconv.Itoa整型 -> 字符串 ,strconv.FormatInt或strconv.FormatUint或strconv.ParseInt或Atoi 
常量 常量有以下几个基本特点:
编译时即对编译器可知 
必须是基础类型:boolean,string或number 
 
常量使用const声明,形式看起来和使用var类似,不过值是常量。对常量进行的所有操作,如数学运算、逻辑运算、比较、内置函数求值,都是在编译期就确定了。
常量可以组声明,声明时可以不显式声明类型,此时将使用右侧操作数推断常量类型。
1 2 3 4 5 6 7 const  (	noDelay time.Duration = 0  	timeout = 5  * time.Minute ) fmt.Printf("%T %[1]v\n" , noDelay)  fmt.Printf("%T %[1]v\n" , timeout)  
还有个不常用的点:组声明时,除了第一个常量,剩下的常量可以不写右侧操作数 ,此时会使用上一个常量 来初始化。
1 2 3 4 5 6 const  (	a = 1  	b 	c = 2  	d ) 
常量生成器iota iota即常量生成器,它从0开始,每次常量声明后加一。利用这个规律可以方便地生成一组常量枚举。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 const  (	Sunday Weekday = iota  	Monday 	Tuesday 	Wednesday 	Thursday 	Friday 	Saturday ) const  (	const  _ = 1  << (10  * itoa) 	KB 	MB 	GB 	TB 	PB 	EB 	ZB 	YB ) 
无类型常量 常量和变量不一样的点在,常量是可以不声明类型的 ,根据常量限定的类型,有下面一些类型:
无类型整型 
无类型Boolean 
无类型rune 
无类型浮点数 
无类型复数 
无类型字符串 
 
常量在使用时,会隐式 转换成需要的类型,并在无法转换时抛出错误。
1 2 3 4 var  f float64  = 3  + i f = 2   f = 1e123   f = 'a'   
实际上,这些无类型常量有一个隐含类型,如:
无类型整型 -> int 
无类型浮点数 -> float64 
无类型复数 -> complex128 
无类型rune -> int32(rune) 
 
聚合类型 基本类型是数据结构的组成“原子”。原子的组合就构成了“分子”——聚合类型:
array 
slices 
maps 
structs 
 
其中array和structs是聚合类型的基础,它们都有着固定大小 。而slice和map则是动态大小。
array 1 2 3 var  a [3 ]int fmt.Println(a[0 ]) fmt.Println(f[len (a)-1 ]) 
类似C风格,array表示由0或多个同一类型元素组成的定长序列 。声明数组时,需要使用常量表达式 作为数组长度。当数组元素全部列出时,可以用...代替长度。元素未声明初始值时,按零值(zero value)处理。
1 2 3 var  q [3 ]int  = [3 ]int {1 , 2 }fmt.Println(1 [2 ])  q = [...]int {3 , 4 , 5 } 
另外,当元素较多时,还可以用index到value的键值对形式声明,未声明的值为零值。下面的例子中,r长度100,除了最后一个元素为-1之外,其余都为0.
若数组数组具有可比性,则数组也具有可比性 。另外,不同长度的数组是不同类型。[4]int和[3]int不是同一类型。
1 2 3 4 5 6 7 import  "crypto/sha256" func  main () 	c1 := sha256.Sum256("x" ) 	c2 := sha256.Sum256("X" ) 	fmt.Printf("\t\n" , c1 == c2) } 
Go中将数组作为参数传递时,传递的是复制的新数组,而不是传入数组的引用 ,这是Go和其他语言不大一样的地方。当然可以通过传入数组指针的方式,实现在函数内修改数组内容。由于数组是定长的,在更多时候,函数参数使用slice类型传入。
slice slice和array类型紧密相关,使用[]T声明。每个slice的底层都基于一个array。slice只是一个指针指向array中的某一个元素作为开始,除此之外,它还有len和cap函数分别用来表示切片长度,和切片最大容量(从切片开始到底层array结尾)。
因此不同slice可以共享同一个array,它们之间可以相互重叠。s[i:j]是从创建slice的方式,遵从左闭右开原则,i和j均可省略,省略时分别表示0和数组最末尾元素。创建可以基于一个array变量或一个array指针或其他slice 。创建超过array范围会引起panic,只超过len(s)则会拓展这个slice。因为string实际上是[]byte切片,所以s[i:j]和substring是一个意思。
从上面可以看到,slice即一个指向数组元素的指针,所以传递一个slice时,可以修改底层array的值。下面这个反转数组的函数不限数组长度:
1 2 3 4 5 6 7 8 9 func  reverse (s []int ) 	for  i, j := 0 , len (s)-1 ; i < j; i, j = i+1 , j-1  { 		s[i], s[j] := s[j], s[i] 	} } s := []int {1 , 2 , 3 , 4  ,5 } reverse(s) fmt.Println(s)  
上面的s是一个切片字面量,和array的区别在于没有声明长度。这种写法实际上会生成以后面值为全部元素的数组,并把切片指向这个数组。类似地,还可以使用make创建一个切片。
1 2 make ([]T, len )make ([]T, len , cap )
由于切片只是引用,从效率和可理解性上考虑,切片间不具有可比性 。不过切片可以和nil比较,nil表示空切片,而非“没有元素”的切片。不过Go中slice相关的函数对待这两种切片行为一样。
1 2 3 4 var  s []int  S = nil   s = []int (nil )  s = []int {}  
append和copyappend函数可以操作slice。如果append之后,slice长度超过了底层array的长度,append会自动拓展底层array长度。另外,append不仅可以追加单个元素,还可以追加任意个元素,或解构后的slice。
1 2 3 4 5 6 7 8 9 10 var  runes []rune for  _, r := range  "Hello, 世界"  {	runes = append (runes, r) } fmt.Printf("%q\n" , runes) var  x []int x = append (x, 1 ) x = append (x, 2 , 3 ) x = append (x, x...) 
在不借助append实现类似append功能时,就需要自己借助cap(x)和make完成底层array的长度扩充。如同下面的一段代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func  appendInt (x []int , y ...int ) 	var  z []int  	zlen := len (x) + len (y) 	if  zlen <= cap (x) { 		z = x[:zlen] 	} else  { 		zcap := zlen 		if  zcap < 2  * len (x) { 			zcap := 2  * len (x) 		} 		z = make ([]int , zlen, zcap) 		copy (z, x) 	} 	copy (z[len (x)], y) 	return  z } 
上面的...表示剩余参数,
借助copy还能实现一些slice的原址操作。
1 2 3 4 func  remove (slice []int , i int ) int  {	copy (slice[i:], slice[i+1 :]) 	return  slice[:len (slice)-1 ] } 
map map即键值对,其中key要求具有可比较性 。map有两种构造方式:make或字面量:
1 2 3 4 5 ages := make (map [string ]int ) ages := map [string ]int  { 	"Alice" : 18 , 	"Bob" : 27  } 
map使用下标访问 ,使用delete删除键。另外,访问不存在的key时,值是value类型的零值 。因此可以免去一些多余的初始化步骤。由于map的值并不是变量,所以不能用&获取地址。
1 2 3 4 5 6 ages["Cindy" ] = 23  delete (ages, "bob" )ages["Dred" ] += 1   _ := &ages["Cindy" ] 
map在遍历时,顺序是随机的。因此如果需要确定顺序,需要事先手动排序。
1 2 3 4 5 6 7 8 9 10 import  "sort" var  names []string for  name := range  ages {	names = append (names, name) } sort.Strings(names) for  _, names := range  names {	fmt.Printf("%s\t%d\n" , name, ages[name]) } 
map的delete,len,range和取值操作都可以对零值nil进行,但是存储到nilmap时会报错。由于访问map不存在的key会返回默认的零值,所以下标操作用第二个参数返回是否对应的key,*且参数通常命名ok*。
1 2 3 if  age, ok := ages["Ed" ]; !ok {	 } 
Go中没有set类型,可以用map[string]bool等价。当key可能不可比较时(如用slice做key),可以用额外的序列化使用。
struct struct类似ts中的interface。由零 或多个fields组成,每个field使用点 来访问。struct和field都是变量,所以可以用&获取地址。对地址也可以使用点来访问field。
1 2 3 4 5 6 7 8 9 10 11 12 type  Employee struct  {	Id int  	Name string  	Address string  	DoB time.Time 	Position string  	Salary int  } var  e Employeee.Salary = 1000  pos := &e.Position *pos = "Senior "  + *pos 
相同类型的两个key可以在一起声明 。在Go的struct中,field的组合和排序都意味着不同的type 。和包一样,大写的field被导出可被访问,这也是Go的一种通用的设计。
1 2 3 4 5 6 7 type  Employee2 struct  {	Id int  	Name, Address string  	DoB time.Time 	Position string  	Salary int  } 
struct类型的field不能自指,但是允许包含自己类型的指针 ,比如最经典的二叉树场景。
1 2 3 4 type  tree struct  {	value int  	left, right *tree } 
struct的零值由各field零值组成,不是nil,没有field的空struct写作struct{}。不携带信息,但可能在有些地方会有用。
字面量struct 两种声明方式:
1 2 3 4 type  Point struct { X, Y int  }p := Point{1 , 2 } anim := git.GIF{LoopCount: nframes} 
将所有fields按顺序 声明,struct的fields有任何改动都需要修改,所以通常只在小规模struct以及包内部使用 
使用键值对方式声明,可以省略field,且对顺序不敏感 
 
另外,在Go中,所有的函数参数传递都是传值 。因此,如果函数内部需要修改struct时,不能传递struct类型,而需要传递指针。由于struct传递指针的场景比较多,所以提供了类似p := &Point{1, 1}的简写语法糖。
1 2 3 func  AwardAnnualPrize (e *Employee) 	e.Salary = e.Salary * 2  + 1000  } 
如果struct的所有field都具有可比性,则struct也具有可比性,可以比较是否相等。因此,struct在有些情况可以用来作为map的key。
struct嵌入与匿名域 匿名域用于struct之间的组合,可以达到类似 类继承的效果。在struct声明中,如果field类型是有名称的,则可以忽略掉field名,得到一个匿名域。匿名域类型或类型内的各field对应用struct可见。有点类似TS中interface的extends。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 type  Point struct  {	X, Y int  } type  Circle struct  {	Point 	Radius: int  } type  Wheel struct  {	Circle 	Spokes int  } var  w Wheelw.X = 8  w.Y = 10  w.Radius = 5  w.Spokes = 20  
换种说法,匿名域就是向下访问时可以省去不写的中间域。即使中间域类型是首字母小写不对外可见的,只要剩下域对外可见也可以访问。struct这种组合 思想是Go在面向对象上的核心。
JSON 编解码JSON数据的方法都位于encoding/json,其中编解码整块数据的函数分别为json.Marshal和json.Unmarshal,前者传入Go数据结构,返回压缩后的JSON字符串,使用json.MarshalIndent可以返回美化后的JSON字符串。编码时,只有被导出的域才会出现在JSON字符串中 。且field之后的field tag 可以作为metadata修改JSON行为,如指定被JSON字符串化之后的key名。或用下面的omitempty忽略掉零值的key。
1 2 3 4 5 6 type  Movie struct  {	Title string  	Year int  `json:"released"`  	Color bool  `json:"color,omitempty"`  	Actors []string  } 
相反,在解码JSON数据时,需要显式声明struct结构来接收JSON数据。json.Unmarshal方法的第二个入参即struct的指针。在解析JSON时,对key是不区分大小写的,因此只需要对a_b类型的JSON key指定field tag 。
对于stream格式的JSON数据,使用json.Encode和json.Decode编解码。
HTML和文本模板 text/template和html/template用于文本模板和HTML模板。它们都使用双花括号包裹带有逻辑的简单语句。其中,html/template还会默认对文本做escape脱敏处理(对template.HTML不会escape)。
使用template.New创建模板 
template.Funcs向模板内插入函数template.Must保证模板有内容template.Parse解析模板使用模板的Execute方法生成解析后内容 
 
1 2 3 4 5 6 7 8 9 10 11 12 var  report = template.Must(template.New("issueList" )).Funcs(	template.FuncMap("daysAgo" : daysAgo) ).Parse(templ) func  main () 	result, err := github.SearchIssues(os.Args[1 :]) 	if  err != nil  { 		log.Fatal(err) 	} 	if  err := report.Execute(os.Stdout, result); err != nil  { 		log.Fatal(err) 	} } 
函数 声明 1 2 3 func  add  (x, y int ) int ) { z = x - y; return  }func  first (x int , _ int ) int  { return  x }func  zero  (int , int ) int  { return  0  }
相同类型入参可以聚合x, y int 
返回值为多个时,需要用()包裹 
返回值也可以给予变量名,这种情况下,相当于提前为返回值声明变量 
入参是传值 ,即入参会复制一份传递给函数内部,只有像slice、map、function、channel这种引用实现的类型在函数内改变会影响外部值 
只有函数声明,没有函数体的函数表示函数由其他语言实现,如func Sin(x float64) float64 
 
递归 Go的递归和其他语言无异。不同的是,传统语言的函数递归借助定长的栈 实现,大小从64KB到2MB不等,而Go使用变长栈 实现,避免的栈溢出的情况。
多返回值 1 func  Size (rect image.Rectangle) int )
Go支持同时返回多个返回值。同类型返回值可以压缩,还可以声明有名称的返回值。在多返回值时,还可以直接传递给需要多个入参的函数,
1 2 3 4 5 log.Println(findLinks(url)) links, err := findLinks(url) log.Println(links, err) 
返回值有名称时,会作为函数体内最外层变量出现 。因此,不需要显示return返回值,这种现象也称为“裸返回”(bare return )。由于裸返回时,return后不会跟随返回值,不利于代码可读性,所以只在需要的时候使用它。
错误 函数返回 错误在Go中是普遍现象。有时,错误类型只需要有1种,这时通常用bool类型的ok表示。如对map类型的变量的访问。但大多数时候,错误原因可能要有比较多种,这时可以用error类型的err表示。
在Go中较少使用exception表示失败(尽管Go也有exception机制),Go只在真正的bug处,才使用异常打印stack trace信息。在Go中较常出现的是普通的error类型,它只作为普通控制流的一部分。
处理策略 error处理由调用方负责,有5种策略:
向上传递 ,在没有error时,可以用fmt.Errorf制造一个自定义错误信息的错误。Go建议仔细设计错误信息内容,不使用大写字母,不使用换行 。建议函数的每一层补充上更多信息。重试 ,在有些场景下,如测试服务端连接退出 ,严重问题时,可以用os.Exit(1)退出,或者用log.Fatalf打印错误信息后退出打印日志后继续 ,对于简单问题,可以打印日志后继续流程忽略 ,在特殊情况下,可以直接忽略,如错误确实不会影响功能实现 
Go建议是使用函数时考虑错误处理的场景。
EOF EOF(End Of File)是一种特殊的错误类型,io.EOF表示输入流没有更多内容了。
1 2 3 4 5 6 7 8 in := bufio.NewReader(os.Stdin) for  {    r, _, err := is.ReadRune()     if  err == io.EOF {         break      }      } 
作为值的函数 
这一章很类似JS或TS
 
Go中函数是一级成员 。这意味着,它可以作为一种类型,传递给变量、入参或者返回,就像其他值的类型一样。函数是一种引用类型,所以可以为nil,但是执行nil会导致panic 。
1 2 var  f func (int ) nil f(2 )  
再次基础上,就可以对函数做更灵活而精准的设计,拆分函数关注点和抽象层次。构造出更灵活的程序。以strings.Map为例
1 2 func  add1 (r rune ) rune  { return  r + 1  }fmt.Println(strings.Map(add1, "Admin" ))  
匿名函数 Go中只能在包级别声明有名函数,而匿名函数可以在块作用域、函数作用域内声明。因此,高阶函数 、闭包 等概念Go中也有。由于这些概念JS中也有,这里就不再赘述。
循环变量捕获 
JS也有类似问题,不过原因不同
 
1 2 3 4 5 6 7 8 var  rmdirs []func () for  _, dir := range  tempDirs() {    os.MkdirAll(dir, 0755 )     rmdirs = append (rmdirs, func ()          os.RemoveAll(dir)     }) } 
上面的for循环中,循环变量dir在append的回调中有使用,我们回忆一下,for循环中循环变量位于for语句块外,在整个for循环后才销毁。所以这会导致每一个回调执行时,dir都被更新为最新的值。将dir在循环体内再次赋值即可。
1 2 3 4 5 6 7 8 9 var  rmdirs []func () for  _, dir := range  tempDirs() {         dir := dir     os.MkdirAll(dir, 0755 )     rmdirs = append (rmdirs, func ()          os.RemoveAll(dir)     }) } 
变长参数 1 2 3 4 5 6 7 func  sum (vals ...int ) int  {    total := 0      for  _, val := range  vals {         total += val     }     return  total } 
类似JS中的剩余参数,Go中也使用rest ...type表示函数的剩余入参。rest需要声明类型,rest为slice类型。要注意的是,这种函数和直接传入一个slice参数的函数类型并不一样。另外,在剩余参数类型不明确时,可以用interface{}表示。
1 2 3 val := []int {1 ,2 ,3 } fmt.Println(sum(values...)) 
延迟函数调用(Deferred Function Calls) 在语句前加上defer标识符,会让defer后的函数调用 推迟到所在函数的**return之后**执行。defer后的函数和表达式会立即求值。defer的函数调用在函数panic后仍然会被调用。可以用来执行一些释放资源的操作,如以下场景:
open和close 
connect和disconnect 
lock和unlock 
 
1 2 3 4 5 6 7 var  mu sync.Mutexvar  m = make (map [string ]int )func  lookup (key string ) int  {    mu.Lock()     defer  mu.Unlock()     return  m[key] } 
最合适的使用时机是在刚刚获得资源之后 。还可以利用defer完成进入 和离开 函数的成对操作做一些调试。
1 2 3 4 5 6 7 8 9 10 11 12 func  bigSlowOperation ()     defer  trace("bigSlowOperation" )()          time.Sleep(10  * time.Second) } 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在函数最后执行的特点,甚至可以在defer中获取和修改函数返回值 。
1 2 3 4 5 func  triple (x int ) int ) {    defer  func ()      return  double(x) } fmt.Println(triple(4 ))  
同时也由于defer的这个特点,在for循环中使用defer一定要谨慎。
panic Go中的panic类似于其他语言的exception,它一般代表程序中存在bug和不应该出现的情况。panic后,正常程序执行停止,defer的函数被倒序执行 ,然后函数崩溃并带有错误信息。
除了系统触发的panic,还可以直接通过panic("certain message")手动触发一个panic。一些包中以Must 开头的API通常表示,在不符合规范的时候API会panic。建议只在内部可信任环境下使用这种API 。
recover 就像其他语言中的try catch一样,Go中的panic同样有机制去妥善处理。Go有内置的recover函数,可以用于在panic中恢复。
recover需要在defer的函数中使用recover函数会返回panic的value,在没有panic的情况下,该函数返回nil 
下面是一些使用recover的建议:
不要毫无条件地从panic中recover,这可能会掩盖一些潜在的bug或资源泄露 
在panic后,可以使用runtime.Stack这样的方法打印一下错误的详细信息,再recover 
可以定义一些外部不可见的类型,在调用panic时传入,在recover返回时判断类型,从而做到针对特定情况panic执行recover,其余情况仍旧panic 
对于预期中的error不使用panic 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func  soleTitle (doc *html.Node) string , err error ) {    type  bailout struct {}     defer  func ()          switch  p := recover (); p {         case  nil :                      case  bailout{}:             err = fmt.Errorf("multiple titles" )         default :             panic (p)          }     }()      } 
方法 Go也有OOP的特性,即对象 上具有方法 ,方法需要关联在一个特定类型上。
声明 1 2 3 4 5 6 import  "math" type  Point struct { X, Y float64  }func  (p Point) float64  {    return  math.Hypot(q.X-p.X, q.Y-p.Y) } 
声明里,在普通声明的函数名前,增加函数绑定的类型receiver,即完成了方法的声明。Go中没有this和self这样的保留字。类型receiver中的变量,即方法可以访问的类型变量。变量名由于会比较常用,所以通常取类型首字母。
其他方法的行为类似其他OOP语言:诸如方法名和函数名不在一个命名空间,所以可以重名;方法名之间不能重名;方法名不能和属性名相同 。由于Go中声明命名类型比较自由,而方法可以很方便绑定在命名类型上,所以可以给基础类型,如数字、字符串等,增加新方法。
指针receiver 上面提到,访问方法需要一个receiver。除了变量本身,指针也可以作为receiver。在Go中函数入参都是传值的,也就是传入值的复制。所以除了map、slice这种引用类型,其余类型的值在方法内改变并不会影响到外部。如果需要方法改变receiver本身的话,可以指定将方法绑定在指针类型上。
1 2 3 4 5 6 7 8 func  (p *Point) float64 ) {    p.X *= factor     p.Y *= factor } t := &Point{1 , 2 } r.ScaleBy(2 ) fmt.Println(*r) 
通常会规定,如果类型上有方法是指针类型的receiver,所有的方法都需要有一个指针类型receiver。类似struct一节介绍的,如果变量具有类型T,而方法的receiver是*T,我们可以直接使用简写的p.ScaleBy(2)而不需要写成(&p).ScaleBy(2)。相反地,receiver要求类型T,变量传入*T也是可以的。
但是,直接将字面量传入给指针类型的receiver是不允许的,如Point{1, 2}.ScaleBy(2)。
Nil是合法的Receiver Go中,nil在很多时候是合法的零值。同样也可以作为receiver。当然在你的命名类型中,最好对合法的nil类型加以说明。Go的内置类型和操作,如slice,map、struct、append、make等也可以正常地处理nil。
组合和struct embedding 在此前的struct一节中,已经介绍了Go的struct embedding设计。这里结合方法继续讨论一下。首先我们先回忆下struct embedding是啥。
1 2 3 4 5 6 7 8 9 10 11 12 13 import  "image/color" type  Point struct { X, Y float64  }type  ColoredPoint struct  {    Point     Color color.RGBA } var  cp ColoredPointcp.X = 1  fmt.Println(cp.Point.X) cp.ScaleBy(2 ) fmt.Print;n(cp.Y) 
匿名的field会直接将field类型中的成员和方法 都组合 (composite)进当前类型中(和TS中的extends有点像)。如上面的ColoredPoint就直接拥有了Point的Distance和ScaleBy功能(当然也可以访问Point)。Go更希望用组合 (composition)而非派生 (derivation)构造更复杂的类型。比如,上面的ColoredPoint并不是一个Point,并不能当做一个Point访问和使用。
匿名field如果是指针类型,除了上面的特性,还能实现让两个变量共享一个底层的结构。
1 2 3 4 5 6 7 type  ColoredPoint struct  {    *Point     Color color.RGBA } p := ColoredPoint{&Point{1 , 1 }, red} q := ColoredPoint{&Point{3 , 4 }, blue} p.Point = q.Point 
在访问receiver上的方法时,Go首先会去直接声明的field中寻找,然后再去embedded的field中寻找,再向下寻找。方法只能在命名类型和其指针类型上定义,但是借助struct embedding也可以实现,将功能聚合 在一起。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 var  (    mu sync.Mutex     mapping = make (map [string ]string ) ) func  Lookup (key string ) string  {    mu.Lock()     v := mapping[key]     mu.Unlock()     return  v } var  cache = struct  {    sync.Mutex     mapping map [string ]string  } {     mapping: make (map [string ]string ), } func  Lookup (key string ) string  {    cache.Lock()     v := cache.mapping[key]     cache.Unlock()     return  v } 
上面重写之后的代码表现力明显更好了。
方法值(method value)和方法表达式(method expression) 1 2 3 4 5 6 p := Point{1 , 2 } q := Point{4 , 5 } distanceFromP := p.Distance fmt.Println(distanceFromP(q)) time.AfterFunc(10  * time.Second(), r.Launch) 
p.Distance会得到一个method value,它是一个绑定到了特定receiver上的一个方法,本身也是一个函数。可以当做函数类型的值用作入参或返回值。这个和JS还比较像。
类似的,Go中还有method expression的概念。即直接用类型名加点(.)访问方法得到一个method expression。它也是一个函数,可以看做是一个没有绑定receiver的方法 。调用函数时,传入的第一个入参会当做receiver,后续的作为方法入参。这个特性在需要根据情况灵活选择方法时很好用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func  (p Point) return  Point{p.X + q.X, p.Y + q.Y} }func  (p Point) return  Point{p.X - q.X, p.Y - q.Y} }type  path []Pointfunc  (path Path) bool ) {    var  op func (p, q Point)      if  add {         op = Point.Add     } else  {         op = Point.Sub     }     for  i := range  path {         path[i] = op(path[i], offset)     } } 
一个实例:bitset 
bytes.Buffer经常用来拼接字符串fmt的print打印字符串时会调用变量的String方法 
封装 首先,Go的封装细粒度只到package一层,package内不控制可见性 。所以当你想要控制可见性时,需要用拆分package实现。
封装用来掩盖不需要像使用者展示的信息和细节。Go中唯一用来控制可见性的手段是一个大小写约定 :大写表示从包中导出,小写表示包内可见,对于struct中的field以及类型的method也是如此(包外访问不了,包内随便访问)。通常来讲,当需要封装对象时,我们都会使用struct 。
1 2 3 4 5 6 type  IntSet struct  {    words []uint64  } type  IntSet []uint64 
使用struct封装本来就很简单的类型有以下几个原因:
使用方无法修改对象值,这样只用查阅更少的声明,就能得到对象值大致的可能范围 
对使用方掩盖实现细节,可以避免让使用方依赖那些可能改变的内容,也给开发者重构空间,开发者可以在不改变API兼容性的情况下灵活调整内部实现 
避免使用方任意修改对象值,造成更多边缘情况,提高程序编写难度和程序不稳定性 
 
有时,封装也会暴露出一些getter 和setter 。通常命名上,会直接使用field的首字母大写形式,省去不必要的Get,Fetch,Lookup前缀。实际上,Go并不禁止导出field,只是在有些情况下,导出field会影响代码可靠性。
封装并不总是必要的。有时,底层数据结构是目标数据结构的充要表示 ,不多不少,场景变数不多,这时可以不用struct封装。但是,像IntSet这种,实现细节变数多,程序实现稳定性低,需要被保护起来,避免影响使用者。从而要采取封装的方式,把那些变数变得“不透明”。
接口(interface) 如其他OOP语言一样,Go中也有用于标识抽象类型的接口描述。不同的是,Go中的接口都是隐式 满足的,松耦合。
作为约定的接口 之前介绍过的所有类型都是具体类型 (concrete type),具体类型指数据表示和行为实现在类型确定后,就已一清二楚。为了保证语言灵活性,Go中还有接口类型 (interface type)。这种类型不暴露内在结构和实现细节,而是给出接口输入输出,作为一种约定 交由具体实现方完成,从而实现依赖反转(DI)。这一概念的设计上和其他OOP语言无二。不过在使用上,Go并不要求实现方明确依赖关系,只需实现约定即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package  fmtfunc  Fprintf (w io.Writer, format string , args ...interface {}) int , error )func  Printf (format string , args ...interface {}) int , error ) {    return  Fprintf(os.Stdout, format, args...) } func  Sprintf (format string , args ...interface  {}) string  {    var  buf bytes.Buffer     Fprintf(&buf, format, args...)     return  buf.String() } package  iotype  Writer interface  {         Write(p []byte ) (n int , err error ) } 
同样的,实现String方法也让类型隐式满足了fmt.Stringer的定义。Go中单方法interface的命名,通常以动词的名词形态为主。
1 2 3 4 5 package  fmttype  Stringer interface  {    String() string  } 
接口中也有类似struct embedding的嵌入式写法 ,简化interface的组合成本。另外,interface中方法的顺序不影响interface类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 package  iotype  Reader interface  {    Read(p []byte ) (n int , err error ) } type  Closer interface  {    Close() error  } type  ReadWriter interface  {    Reader     Writer } 
接口的满足 Go中变量可以是接口类型,在给接口类型变量赋值时,需要检查值的方法是否满足了接口的类型定义,这一点和其他OOP语言相似。要注意,尽管Go有将变量T转成*T的隐式转换,但是类型T的方法和*T的方法receiver并不同。而通常具体类型中会在接口声明的方法中做一些写操作,因而指定receiver为指针类型,这有可能会导致无法满足接口定义。
接口覆盖了其包裹的内部类型,所以,即使内部类型满足其他方法,赋值给接口类型后,也只能方法接口拥有的方法 。
1 2 3 4 var  w io.Writerw = os.Stdout w.Write([]byte ("hello" ))  w.Close()  
Go中还有一个通用的不可或缺的类型interface{}它表示对类型没有任何要求,同时也意味着该类型变量上无法执行任何操作,类似ts中的Unknown。
1 2 3 4 5 var  any interface {}any = true  any = 12.34  any = "hello"  any = new (bytes.Buffer) 
Go中具体类型对接口类型的满足都是隐式的,无需显式声明。所以一个具体类型可能会同时满足很多接口类型。可以把接口类型认为是将一些具体类型中公共的部分抽象出来的共同行为,将之作为grouping 出来的共性。
使用flag.Value解析命令行参数 
fmt.Sscanf可以从输入中按格式解析出特定类型参数 
接口值 1 2 3 4 var  w io.Writerw = os.Stdout w = new (bytes.Buffer) w = nil  
Go中,接口类型可以作为变量的合法类型。接口类型值因此具有动态类型 和动态值 。在Go中可以近似用类型描述符(type descriptor)表示,其中type表示具体类型,value表示具体值。在初始化时,type和value都是nil。
1 2 3 4 5         +-----------+ type    |    nil    |         +-----------+ value   |    nil    |         +-----------+ 
而在第二和第三行,为w赋值为os.Stdout以及*bytes.Buffer类型时,type分别会变成os.Stdout和*bytes.Buffer,同时value也会被设置为对应初始值的指针。这个过程会完成类似于io.Writer(os.Stdout)的隐式类型转换。此时访问w的方法,会被动态分配到value上实现。而在最后又将w还原为初始值nil。
接口类型之间不一定 可以比较,当接口值都为nil或接口值对应的具体类型相同以及具体值相同时,接口值相同。然而,如果具体类型不可比较时(如slice,function等),接口类型也不可比较。Go的fmt中,可以用%T打印变量类型。
陷阱:nil值可以存在于非nil的接口值中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const  debug = true func  main ()     var  buf *bytes.Buffer     if  debug {         buf = new (bytes.Buffer)     }     f(buf)     if  debug {              } } func  f (out io.Writer)     if  out != nil  {         out.Write([]byte ("woops!\n" ))      } } 
在上面的判断中,out已经有了具体的类型,因此接口类型的out不等于nil,然而out的具体值却是nil,这使得Write行为无法保证。解决办法是,在一开始为buf声明为io.Writer类型即可。
sort.InterfaceGo使用sort包中的sort.Interface实现排序功能。同时对于常见类型string、int等也有事先封装好的sort.Strings(),sort.Int()。对于自定义类型,在实现sort.Interface接口后,也可使用sort.Sort排序。接口定义如下:
1 2 3 4 5 6 7 package  sorttype  Interface interface  {    Len() int      Less(i, j int ) bool      Swap(i, j int ) } 
三个方法分别用来返回长度 、比较大小 和交换顺序 。这也是排序的几个基本操作。下面给出了字符串排序的内部实现:
1 2 3 4 5 type  StringSlice []string func  (s StringSlice) int  { return  len (s) }func  (s StringSlice) int ) bool  { return  p[i] < p[j] }func  (s StringSlice) int ) { p[i], p[j] = p[j], p[i] }
在排序struct等复杂类型slice时,建议定义指针类型数组,这样可以让swap时速度更快 。
sort还有一个方便的反向排序方法sort.Reverse,它借助了struct embedding,用一个内部类型reverse封装了外部实现接口的类型,另外,直接在reverse上定义了Less,覆盖了Interface的实现,从而实现了反向排序:
1 2 3 4 5 6 package  sorttype  reverse struct  { Interface }func  (r reverse) int ) bool  { return  r.Interface.Less(j, i) }func  Reverse (i Interface) return  reverse{i} }sort.Sort(sort.Reverse(byArtist(tracks))) 
除了slice类型外,其他实现了sort.Interface接口的类型一样可以排序:
1 2 3 4 5 6 7 8 type  customSort struct  {    t []*Track     less func (x, y *Track) bool  } func  (x customSort) int  { return  len (x.t) }func  (x customSort) int ) bool  { return  x.less(x.t[i], x.t[j]) }func  (x customSort) int ) { x.t[i], x.t[j] = x.t[j], x.t[i] }
http.Handler接口1 2 3 4 5 6 package  httptype  Handler interface  {    ServeHTTP(w ResponseWriter, r *Request) } func  ListenAndServe (address string , h Handler) error 
实现了这个接口的可以传递给ListenAndServe。但通常用不到这种原始的方式。Go的http包提供的ServeMux类型可以给请求分路,聚合一堆http.Handlers。写起来像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func  main ()     db := database{"shoe" : 50 , "socks" : 5 }     mux := http.NewServeMux()     mux.Handle("/list" , http.HandlerFunc(db.list))     mux.Handle("/price" , http.HandlerFunc(db.price)) } type  database map [string ]int func  (db database)     for  item, price := range  db {         fmt.Fprintf(w, "%s: %s\n" , item, price)     } } func  (db database)      } 
其中http.HandlerFunc将传入的函数包裹成了满足Handler接口的类型。
1 2 3 4 5 6 7 package  httptype  HandlerFunc func (w ResponseWriter, r *Request) func  (f HandlerFunc)     f(w, r) } 
对于上面的使用还是要写一些模板代码,对此可以将mux.Handler简写成mux.HandlerFunc("list", db.list)。可以再减少一点代码。实际上,http还提供了一个全局的ServeMux对象实例DefaultServeMux,不需要手动创建。
1 2 3 4 5 6 func  main ()     db := database{"shoes" : 50 , "socks" : 5 }     http.HandlerFunc("/list" , db.list)     http.HandlerFunc("/price" , db.price)     log.Fatal(http.ListenAndServe("localhost:8000" , nil ) } 
go中每一个handler都在一个单独的goroutine上,要妥善处理好并发的情况。 
error接口1 2 3 type  error  interface  {    Error() string  } 
error类型实现了error接口。整个errors包都围绕这个接口设计,除了errors.New()方法,还可以直接通过fmt.Errorf返回一个格式化后的error值。
1 2 3 4 5 6 7 8 9 10 11 12 package  errorsfunc  New (text string ) error  { return  &errorString{text} }type  errorString struct  { text string  }func  (e *errorString) string  { return  e.text }package  fmtimport  "errors" func  Errorf (format string , arags ...interface {}) error  {    return  errors.New(Sprintf(format, args...)) } 
简单的数学表达式求值器 递归的AST解析
略。
类型断言 类型断言(type assertion),写作x.(T),通常用来将动态类型限定到更严格的类型。
T是具体类型时,会判断x类型是否和T一致 ,是则将x类型设置为T,否则panicT是抽象类型interface时,会判断x是否满足 T接口,是则将x类型设置为接口T,否则panic 
1 2 3 4 5 6 7 var  w io.Writerw = os.Stdout f := w.(*os.File) c := w.(*bytes.Buffer)  w = new (ByteCounter) rw := w.(io.ReadWriter)  
当对nil进行类型断言时时,断言一定失败 。另外,类型断言可以支持第二个返回参数ok表示是否成功,此时不会panic。
1 2 3 if  w, ok := w.(*os.File); ok {     } 
应用:错误类型区分 借助类型断言,可以将判断抛出的具体错误类型,os包提供了IsExist,isNotExist,isPermission用来区分文件已存在,文件不存在,不允许几种错误。我们以文件不存在为例,此时抛出的PathError类型错误包含了具体的错误类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 type  PathError struct  {    Op string      Path string      Err error  } func  (e *PathError) string  {    return  e.Op + " "  + e.Path + ": "  + e.Err.Error() } _, err := os.Open("/no/such/path" ) fmt.Printf("%#v\n" , err) 
使用断言后,就可以从err中拿到具体错误类型,从而判断是否是文件不存在导致的:
1 2 3 4 5 6 7 8 9 10 import  (    "errors"      "syscall"  ) func  IsNotExist (err error ) bool  {    if  pe, ok := err.(*PathError); ok {         err = pe.Err     }     return  err == syscall.ENOENT || err == ErrNotExist } 
另外,建议在错误抛出时就进行检测,在聚合后,原始错误的数据结构可能会丢失从而无法判断。
方法查询 抽象类型如io.Writer可能缺少使用者需要的方法如io.WriteString(尽管满足io.Writer的大多数具体类型除了必须满足的Write方法外,都对写入字符串支持了WriteString方法)。
可以定义一个临时接口类型,判断满足抽象类型的变量是否具有指定方法。因为Go中接口的满足是隐式的(类似鸭子类型),不像许多强类型语言一样,需要显式声明。之前使用弱类型语言的可能能很好接受。
1 2 3 4 5 6 7 8 9 func  writeString (w io.Writer, s string ) int , err error ) {    type  stringWriter interface  {         WriteString(string ) (n int , err error )     }     if  sw, ok := w.(stringWriter); ok {         return  sw.WriteString(s)      }     return  w.Write([]byte (s))   } 
实际上,fmt.Sprintf打印不同类型的变量时,也借助了类型断言,对于特定类型调用特定方法,最后再使用反射 处理其他类型。
1 2 3 4 5 6 7 8 9 10 11 package  fmtfunc  formatOnValue (x interface {}) string  {    if  err, ok := x.(error ); ok {         return  err.Error()     }     if  str, ok := x.(Stringer); ok {         return  str.String()     }      } 
Type switch interface除了之前说的让多个具体类型有一致表现 的用法外,还可以作为可区分具体类型的合集 来使用。这种时候需要结合type switch的用法。如下所示:
1 2 3 4 5 6 7 switch  x.(type ) {case  nil : case  int , uint : case  bool : case  string : default : } 
通常在确定了x的类型后,还需要直接使用x。此时可以写作switch x:= x.(type)。
这种用法和之前的用法不同在于:这里接口不作为有一致表现 而存在,它只是用来暂存将要区分开的具体类型,而这些具体类型往往时有不同表现 的。所以这种用法里的接口几乎没有方法 。换一种说法,之前的用法里,接口背后的具体类型细节需要被掩盖来使用,而这里需要使用具体类型的细节。
一些忠告 和上一章方法类似,接口是一种很好使用的面向对象的特性。但不建议上来就从定义一堆接口 开始,这样通常会产生一大堆只有一个具体类型实现的接口。接口是抽象类型,是通过具体类型抽象得来的。通常是在需要用统一的方式处理不同类型 时,拿来使用。
同时,大多数Go程序中,接口往往小且包含比较少的方法。像是io.Writer或fmt.Stringer。和方法一章一样,它们虽然是面向对象的特性,但是不是Go中一定要使用的语言特性。只在需要的时候使用。大多数时候,直接使用函数就足够了。在书中,方法如input.Write的使用就远不如函数如fmt.Printf来得频繁。
goroutine和信道 Go支持两种并发编程的风格,第一种在本章介绍,通过goroutines和channels支持通信顺序进程(Communicating sequential processes,CSP) ,这种情况下,值会在goroutine间来回传递,而变量在多数情况下被限制自单个活动中。下一章介绍的共享变量风格的并发编程更接近传统的并发风格。
认识goroutine 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func  main ()     go  spinner(100  * time.Millisecond)     const  n = 45      fibN := fib(n)      fmt.Printf("\rFibonacci(%d) = %d\n" , n, fibN) } 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 ) } 
goroutine类似线程,有着定量而非定性的差异 
main函数也会启动一个main goroutine goroutine通过go启动一个函数或方法调用,并在声明后立即返回 
除了main函数返回或程序结束(os.Exit)外,一个goroutine没有办法直接停止另一个,但可以通过传值的方式间接实现。 
 
简单示例 服务器处理请求是最典型的并发场景。
1. Clock Server 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package  mainimport  (    "io"      "log"      "net"      "time"  ) func  main ()     listener, err := net.Listen("tcp" , "localhost:8000" )     if  err != nil  {         log.Fatal(err)     }     for  {         conn, err := listener.Accept()         if  err != nil  {             log.Print(err)             continue          }         handleConn(conn)      } } func  handleConn (c net.Conn)     defer  c.Close()     for  {         _, err := io.WriteString(c, time.Now().Format("15:04:05\n" ))         if  err != nil  {             return           }         time.Sleep(1  * time.Second)     } } 
listener.Accept会在接收到TCP连接请求前一直阻塞time.Format方法通过一个特殊的样例 (15:04:05)表示要格式化的格式,time.Parse也是如此client端可以用net.Dial发起一个TCP连接请求 
 
上述的服务端是串行 处理client的请求,并每秒打印当前时间,在handleConn(conn)前加上go关键字后,即可让服务端并行 处理client的请求。
2. Echo Server 上面的例子是在一个连接中使用一个goroutine,当然每个连接也可以创建多个goroutine。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func  echo (c net.Conn, shout string , delay time.Duration)     fmt.Fprintln(c, "\t" , strings.ToUpper(shout))     time.Sleep(delay)     fmt.Fprintln(c, "\t" , shout)     time.Sleep(delay)     fmt.Fprintln(c, "\t" , strings.ToLower(shout)) } func  handleConn (c net.Conn)     input := bufio.NewScanner(c)     for  input.Scan() {         echo(c, input.Text(), 1 *time.Second)      }     c.Close() } func  main ()     conn, err := net.Dial("tcp" , "localhost:8000" )     if  err != nil  {         log.Fatal(err)     }     defer  conn.Close()     go  mustCopy(os.Stdout, conn)     mustCopy(conn, os.Stdin) } 
在echo前加上go即可让服务器同时相应多个请求,返回“回声”。同理,在client端打印服务端返回的代码前加上go即可让使用者输入的同时打印返回的“回声”。
信道(channel) go启动并行的活动,信道作为活动间通信的通道,借助它可以发送和接收消息。信道通过make构造,需要指定传输消息的类型,作为信道类型。可以使用close关闭信道。后续的发送 操作会panic,接收 操作会得所有到已发送的值,而再之后的后续接收 操作只能得到信道类型对应的零值 。
1 2 3 4 5 6 7 8 ch := make (chan  int ) ch <- x x = <-ch <-ch close (ch)
信道还分为有缓冲区和无缓冲区两种类型,上述的make构造的都是无缓冲区的信道,指定第二个参数可以构造有缓冲区的信道。
1 2 3 ch = make (chan  int )  ch = make (chan  int , 0 )  ch = make (chan  int , 3 )  
无缓冲信道(Unbuffered Channels) 向无缓冲区发送消息 会阻塞发送所在的goroutine ,直到对应的goroutine在同一个信道上执行接收操作。相反地,接收消息 在先的话,也会阻塞直到同一个信道上执行了发送操作。这种机制会同步 两个goroutine的执行进度。如果发送信息在先,则接收信息会在发送所在的goroutine之前发生。从而,我们可以基于这个假设的前提保证一些事实。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func  main ()     conn, err := net.Dial("tcp" , "localhost:8000" )     if  err != nil  {         log.Fatal(err)     }     done := make (chan  struct {})     go  func ()          io.Copy(os.Stdout, conn)         log.Println("done" )         done <- struct {}{}      }()     mustCopy(conn, os.Stdin)     conn.Close()     <-done  } 
上述程序里,会在接收完服务端返回后,才会关闭客户端。这里需要的是一个事件 ,使用的信道类型其实并不重要,所以使用了struct{}。实际应用中会使用bool或是int这样的简单类型。
流水线 借助上面提到的无缓冲区信道,可以实现多个goroutine之间的接续传递,也可以叫做流水线 。
1 2 3 +-----------+         +-----------+         +-----------+ |  Counter  |   -->   |  Squarer  |   -->   |  Printer  | +-----------+         +-----------+         +-----------+ 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 func  main ()     naturals: make (chan  int )     squares: make (chan  int )          go  func ()          for  x := 0 ; x < 100  ;x++ {             naturals <- x         }         close (naturals)     }()          go  func ()          for  {             x := <-naturals             squares <- x * x         }     }     for  {         fmt.Println(<-squares)     } } 
上面的流水线中,Counter在打印100个自然数后,会关闭信道。会有之前所说的一些特性:
向关闭信道写入消息会panic 
从关闭信道读取信息会得到所有未发送的消息,再之后只能得到零值 
关闭信道不会影响其他goroutine执行 
 
所以在上面的程序中,Printer会继续打印0,只有Counter正常退出。Go中没有直接 的获取信道是否关闭的方法,但是对于从信道中读取消息有第二个ok参数,为false时表示信道已关闭且读取完所有消息 。
1 2 x, ok := naturals if  !ok {}
上面的模板代码,go用range已封装好,不必重复书写。
1 2 3 4 5 6 7 8 go  func ()     for  x := range  naturals {         squares <- x * x     } }() for  x := range  squares {    fmt.Println(x) } 
不是所有信道在不用后都要显式关闭 ,只在需要传达信道关闭信息时再手动close关闭。其余的信道会在gc过程中回收。但这不意味着文件读取也可以不显式关闭:文件的读写操作后一定要执行关闭操作。关闭一个已关闭的信道会panic,关闭nil的信道也是一样。
单向信道 以上一小节为例,有三个goroutine,函数签名如下:
1 2 3 func  counter (out chan  int ) func  squarer (out, in chan  int ) func  printer (in chan  int ) 
其中的信道入参分别用来接收 或 发送 消息(绝大多数信道也是如此)。因此对于这两种信道的细分,go类型系统提供了单向信道类型 ,即只读或只写。同时提供了类型助记符:
chan<-表示只读,只可读取消息,不可关闭<-chan表示只写,只可发送消息和关闭  
违背只读只写上述规则,会在编译期间抛出错误。同时,双向信道可以隐式covert到单向信道,反之不可以 。
1 2 3 4 5 6 7 func  main ()     naturals := make (chan  int )     squares := make (chan  int )     go  counter(naturals)     go  squarer(squares, naturals)     printer(squares) } 
缓冲信道(Buffered Channel) 1 ch = make (chan  string , 3 ) 
可以用队列类别缓冲信道,不同的是缓冲信道和goroutine是紧密相连的。
写操作会在队列充满 时阻塞 
读操作会在队列为空 时阻塞 
 
通过cap和len可以查看缓冲信道的实时容量和长度。虽然缓冲信道可以按队列去理解,但是不要把它拿去当队列来用。那么和无缓冲信道相比,缓冲信道应用场景有什么不同呢?
我们用流水线举例,流水线上的各道工序复杂程度有难有易,如果工作空间有限,每一道工序后都需要在下一道工序空闲时才能交付,一些简单工序就需要等待。这时就像无缓冲信道 。假设工作空间宽裕,每道工序完成后,如果下游还未就绪,可以先放在空闲空间下,直接继续工作。这就是缓冲信道 ,多出来的工作空间即缓冲区 ,工序即goroutine 。缓冲区可以弥补上下游工序工作效率的些微差异 ,缓冲区越大,可以容忍的效率差异就越大。如果工序间有明显差异,比如始终更快或更慢,此时增加缓冲区无法提供帮助,可以采用增加工序工人来提高工作效率,即在同一信道上使用更多goroutine 。
从上面的比喻,可以得出两种信道的区别:
无缓冲信道重点在同步 ,它可以确保上下游goroutine的同步性 
缓冲信道则使用了队列来解耦 上下游goroutine,使之不因为阻塞影响工作效率 
 
所以,我们假设有多个网站镜像 来为网络请求提供服务,就可以使用缓冲信道,优先响应的可以直接提供服务,且在响应后可以继续工作。
1 2 3 4 5 6 7 8 func  mirroredQuery () string  {    responses := make (chan  string , 3 )     go  func () "asia.gopl.io" ) }()     go  func () "europe.gopl.io" ) }()     go  func () "america.gopl.io" ) }()     return  <-responses } func  request (hostname string ) string ) {  }
并行循环 有些任务可以拆分成等效的相互独立 的小任务,这种情况也被称为“令人尴尬的并行”,是最简单的并行工作场景,它的工作量和并行数呈线性关系。我们假设有一个并行处理图片缩小的程序,能返回缩小后的文件总体积,并在合适的时候停止。在程序编写过程中,会遇到一些问题:
有错误出现时,未关闭剩余信道,导致goroutine泄露,并造成程序不响应或内存耗尽 
for循环结合延迟执行代码时,循环描述体中的变量陷阱 
要支持任意长度的图片列表,不能写死缓存信道的容量 
无从直接得知goroutine是否执行完成 
 
最终得到下面的最终版本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 func  makeThumbnails (filenames <-chan  string ) int64  {    sizes := make (chan  int64 )     var  wg sync.WaitGroup     for  f := range  filenames {         wg.Add(1 )                           go  runc(f string ) {                          defer  wg.Done()             thumb, err := thumbnail.ImageFile(f)             if  err != nil  {                 log.Println(err)                 return              }             info, _ = os.Stat(thumb)             sizes <- info.Size()         }     }               go  func ()          wg.Wait()         close (sizes)     }()     var  total int64      for  size := range  sizes {         total += size     }     return  total } 
上面程序里面有几点需要特别说明:
第一个for循环会将信道输入的图片文件列表转成任务的列表,然后再启动一个goroutine负责关闭信道,最后从信道中拿出所有的大小加总返回 
关闭函数必须写成goroutine的形式。因为sizes的range结束依赖于sizes信道的关闭,同时sizes信道又必须等待 所有图片处理任务执行完之后再关闭。等待和加总图片大小需要并行,所以需要一个新的goroutine去做 
任务完成借助sync.WaitGroup完成,wg.Wait()会阻塞直到wg.Done()将所有任务清零 
 
样例:并发web爬虫 将第5章中的worklist由slice改为channel,让爬取网页内容的过程并发执行即可得到一个并发的web爬虫。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func  crawl (url string ) string  {    fmt.Println(url)     list, err := links.Extract(url)     if  err != nil  {         log.Print(err)     }     return  list } func  main ()          worklist := make (chan  []string )          go  func () 1 :] }()     seen := make (map [string ]bool )          for  list := range  worklist {         for  _, link := range  list {             if  !seen[link] {                 seen[link] = true                  go  func (link string )                      worklist <- crawl(link)                 }(link)             }         }     } } 
上面的程序在执行了一段时间后,会因为客观限制出现报错信息。这是因为程序过于并发 了。由于硬件资源的限制,当并发数超过一定界限后,程序性能反而不如以前甚至会无法运行。因此需要手动限制并发量。这里有两种思路:
通过限制发放许可证(token)的方式限制爬取goroutine是否执行,许可证数量有限,许可证用完后,阻止goroutine执行。当然作为信道的token,是在多个爬取goroutine间共享的。 
限制爬取goroutine总数,只创建固定个数的goroutine 
 
1 2 3 4 5 6 7 8 var  tokens = make (chan  struct {}, 20 )func  crawl (url) string  {    fmt.Println(url)     token <- struct {}{}     list, err := links.Extract(url)     <-tokens } 
将上面代码main函数中的worklist延迟在for循环内赋值,使用n记录当前任务中的正在执行的任务数,可以实现在所有任务执行完成后退出程序。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 func  main ()          var  n int      n++          seen := make (map [string ]bool )     for  ; n < 0 ; n-- {         list := <-worklist         for  _, link := range  list {             if  !seen[link] {                 seen[link] = true                  n++                 go  func (link string )                      worklist <- crawl(link)                 }(link)             }         }     } } 
select实现多工在之前的例子里,从信道中读取/写入值,会阻塞当前goroutine进度。如果需要同时接收两个信道的值,需要select语句块。select语句块使用类似switch。
1 2 3 4 5 6 7 8 9 10 select  {case  <- ch1:     case  x := <- ch2     case  ch3 <- y:     default :     } 
每一个case可以是接收或是发送消息的语句,select语句在其中一个case发生后,才会继续(select{}会一直等待程序执行)。default可以指定没有任何一个case发生时的处理方式。
原文中给出的time.Tick例子会返回一个channel,并以设定的时间间隔发送消息。但是,再不从channel读取信息后,会造成goroutine泄露。因此只在整个生命周期都需要时才会使用。倒计时这种场景下建议使用更复杂的方式:
1 2 3 ticker := time.NewTicker(1  * time.Second) <- ticker.C ticker.Stop() 
对于一个nil信道的发送和接收会一直阻塞,select中的case也不会被选中。利用这个特性可以实现取消等功能。
并发目录遍历 借助ioutil.ReadDir可以实现遍历根文件夹下所有文件体积的功能。下面是一个纯单线程版本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func  walkDir (dir string , fileSizes chan <- int64 )     for  _, entry := range  dirents(dir) {         if  entry.IsDir() {             subdir := filepath.Join(dir, entry.Name())             walkDir(subdir, fileSizes)         } else  {             fileSIzes <- entry.Size()         }     } } func  dirents (dir string )     entries, err := ioutil.ReadDir(dir)     if  err != nil  {         fmt.Fprintf(os.Stderr, "du1: %v\n" , err)         return  nil      }     return  entries } 
上面的版本可以实现功能,但是速度很慢,而且不能实时显示进度。这里我们用time.Ticker定时打印进度,同时通过命令行参数p控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var  progress = flag.Bool("v" , false , "show progress" )func  main ()          var  tick <-chan  time.Time     if  *progress {         tick = time.Tick(500  * time.Millisecond)     }     var  nFiles, nBytes int64  loop:     for  {         select  {         case  size, ok := fileSizes:             if  !ok {                 break  loop             }             nFiles++             nBytes += size         }         case  <-tick:             printDiskUsage(nFiles, nBytes)     }     printDiskUsage(nFiles, nBytes)  } 
其中if *progress语句在没有传递-p参数时,不会为tick赋值,而nil的tick值会让select中永远不会进入这个case,从而不打印进度。
在遍历根目录下的递归调用walkDir中,也可以使用goroutine,并通过sync.WaitGroup保证执行完成后关闭fileSizes信道。当然,无限制的创建goroutine会出现和上上小节一样的问题,所以也需要信号量(semaphore) ,保证不至于创建过多goroutine。
1 2 3 4 5 6 var  sema = make (chan  struct {}, 20 )func  dirents (dir string )     sema <- struct {}{}      defer  func ()  } 
取消 一样的,一个goroutine没有直接关闭另一个goroutine的办法。按照之前提到的通过信道传递消息 的思路,但是一个信道只会被消费一次,我们这里的场景需要广播 更合适。
之前提到,一个被关闭的信道在传递完信道内的消息后,后续再从这个信道获取值,会立即返回一个零值。可以利用这个特性,在执行取消操作后,将信道关闭即可,可以写出下面这样的函数。
1 2 3 4 5 6 7 8 9 var  done = make (chan  struct {})func  cancelled  () bool  {    select  {     case  <- done:         return  true ;     default :         return  false ;     } } 
然后在程序的瓶颈处 ,检查这个函数的返回值,一旦返回true则立即中止程序。比如,之前提到获取token的函数里。
1 2 3 4 5 6 7 8 9 func  dirents (dir string )     select  {     case  sema <- struct {}{}:      case  <-done:         return  nil ;     }     defer  func ()       } 
按上面这种方式退出程序后,有可能出现goroutine还没有妥善关闭的情况,可以在调试时,程序的最后用panic打印系统信息,查看具体情况。
样例:聊天服务器 聊天服务器也是并发和各种信道常用的场景,它包括:
用户的接入、退出 
用户信息的广播 
用户session的维护 
 
我们可以用一个信道表示一个接入的用户,在一个全局的文件中处理用户登入、登出,即信道的信息维护,这里可以用map表示,对于接收到的消息,像注册的所有信道逐个发送,即广播。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type  client chan <- string  var  (    entering = make (chan  client)     messages = make (chan  string )     leaving = make (chan  client) ) func  broadcaster ()     clients := make (map [client]bool )      for  {         select  {         case  cli := <-entering:             clients[cli] = true          case  cli := <-leaving:             delete (clients, cli)             close (cli)         case  msg := <-messages:             for  cli := range  clients {                 cli <- msg             }         }     } } 
同时,启动一个tcp服务器,单独启动一个goroutine负责上面的信道管理,另外对于每一个接入的连接,启动一个独立的goroutine处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 func  main ()     listener, err := net.Listen("tcp" , "localhost:8000" )     if  err != nil  {         log.Fatal(err)     }     go  broadcaster()     for  {         conn, err := listener.Accept()         if  err != nil  {             log.Print(err)             continue          }         go  handleConn(conn)     } } 
在处理tcp连接的函数里,负责接入客户端,同时将连接中的内容写入到messages信道中,以便广播给其他客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 func  handleConn (conn net.Conn)     ch := make (chan  string )     go  clientWriter(conn, ch)          who := conn.RemoteAddr().String()     ch <- "You are "  + who     messages <- who + " has arrived"      entering <- ch     input := bufio.NewScanner(conn)     for  input.Scan() {         messages <- who + ": "  + input.Text()     }          leaving <- ch     messages <- who + " has left"      conn.Close() } func  clientWriter (conn net.Conn, ch chan  string )     for  msg := range  ch {         fmt.Fprintln(conn, msg)     } } 
上面的map没有使用lock操作,是因为它的读写都限制在了一个goroutine内,因此是并发安全 的,其他并发使用的信道和net.Conn也是并发安全的。
并发和共享变量 使用信道在goroutine间沟通是一种并发的范式,其中也略过了一些关键而细小的问题,这些在后面这种并发编程模式中会经常讨论。
竞险(race conditions) 1 2 3 4 5 6 7 8 package  bankvar  balance int func  Deposit (amount int )     balance += amount } func  Balance () int  { return  balance }
上面以银行为例,实际上给出了一个可以读写的变量。在串行执行场景下,不会有问题。在并发场景下,对balance读写的同时进行,就会造成一些问题。这种情况也叫数据争用(data race) ,即有两个goroutine并发访问一个变量,且至少有一个是写操作 。这种数据争用有时候还会带来未定义的行为。
在使用共享变量的模式并发编程时,如果不小心处理,很容易遇到数据争用的情况。然而,绝大多数的数据争用都“来者不善”,以至于我们要留心发生数据争用的场景:有两个goroutine并发访问一个变量,且至少有一个是写操作 。下面有三种方式去避免:
不要写变量 ,比如将变量初始化好之后,使之只读或不可变避免在多个goroutine上操作变量 ,将操作限制在一个goroutine上,就像前一章中的broadcaster,这样的goroutine也叫做调度者goroutine 。Go中有句箴言总结的很好:不要通过共享变量传递消息,通过传递消息来共享变量 。这里的传递消息就是指通过信道发送和接收。当实在无法限制多个goroutine访问一个变量,也尽量限制访问,通过信道传递给其他goroutine,达到串行限制(serial confinement) 的效果。在同一时间仅允许一个goroutine访问变量 ,即后面会提到的互斥锁  
互斥锁(sync.Mutex) 互斥锁和之前提到的信号量(counting semaphore)很类似,更像是一个容量为1的信号量,即二进制信号量(binary semaphore) 。每次执行后续操作前,都需要从一个全局信道中获取token,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 var  (    sema = make (chan  struct {}, 1 )     balance int  ) func  Deposit (amount int )     sema <- struct {}{}      balance = balance + amount     <-sema  } func  Balance () int  {    sema <- struct {}{}      b := balance     <-sema      return  b } 
将上面的信号量表示使用sync.Mutex替代就是互斥锁的使用方式:
var sema = make(chan struct{}, 1) => var mu sync.Mutexsema <- struct{}{} => mu.Lock()<-sema => mu.Unlock() 
通常来说,互斥锁使用的范围很小,这一区域也叫临界区(critical section),被mutex守护的共享变量会紧跟在Lock之后。在程序较长时,为了避免在所有返回处显式Unlock可以使用defer,这会稍微增加一些显式Unlock的成本,但会让代码更简洁。
另外,互斥锁是不可重入的,即不能对一个已经上锁的共享变量上锁,这会导致死锁,因此确保互斥锁和其守护的变量不被导出。
读/写互斥锁(sync.RWMutex) 1 2 3 4 5 6 7 8 var  mu sync.RWMutexvar  balance int func  Balance () int  {    mu.RLock()     defer  mu.RUnlock()     return  balance } 
sync.RWMutex可以限制写操作,而允许多个读操作同时进行。RLock方法开启,RUnlock关闭互斥锁。注意,只在确定没有对共享变量写操作发生的时候使用RLock方法,我们不能简单的假设一个逻辑 读操作,在背后没有注入写入缓存或更新计数器等行为。如果不确定,请使用完整的互斥锁。
同时,sync.RWMutex只在大多数读操作在争用锁时会比较合适。其更复杂的实现,让它在其他场景下工作慢于普通的互斥锁。
内存同步 上面提到的对于Balance这个只读的函数也使用的互斥锁或者信道来限制多个goroutine访问共享变量,其中一个明显的原因是:读取操作发生在写操作如Withdraw或Deposit中间时,也会造成问题。另一个不那么明显的原因是,类似互斥锁、信道这种同步操作也会同步内存。
简单点说,在现代CPU架构中,多个处理器内很可能有缓存,每个goroutine对共享变量的修改很可能在多个缓存中,而对其他goroutine不可见,直到同步操作把缓存中的修改同步到主内存中,保证对所有goroutine可见且一致。
同一个goroutine内部是串行稳定的,但goroutine之间无法保证顺序。还有一种错误 认识,goroutine的代码会逐行交错 (interleaving)执行。但在现代的CPU架构和编译器中,并不是这么实现的。总而言之,把对变量的使用限制在同一个goroutine内,对其他变量使用互斥锁。
懒初始化(sync.Once) 1 2 3 4 5 6 7 8 9 10 var  icons map [string ]image.Imagefunc  loadIcons ()      } func  Icon (name string )     if  icons == nil  {         loadIcons()     }     return  icons[name] } 
通常来说,我们会推迟一个计算量比较大的初始化操作到使用时才进行,如上面Icon函数做的那样。很显然Icon函数不是并发安全的。在其中混有读写操作,且和外界共享icons变量。这时我们需要在初始化的时候对loadIcons函数加锁。加锁时要区分icons的是否初始化状态,可以对只读操作使用读/写锁,再对写入操作使用互斥锁。像下面这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var  mu sync.RWMutexfunc  Icon (name string )     mu.RLock()     if  icons == nil  {         icon := icons[name]         mu.RUnlock()         return  icon     }     mu.RUnlock()     mu.Lock()          if  icons == nil  {         loadIcons()     }     icon := icons[name]     mu.Unlock()     return  icon } 
实际上,上面就是一个只做一次的操作(通常是初始化操作),为了维护一个是否完成的bool值,额外增加了一些操作,较容易出错。go对这种情况提供了sync.Once支持,在Do方法中传入只执行的函数,这个互斥锁会在第一次执行时上锁并将对变量的改动同步到其他goroutine中,同时维护一个bool值,在后续的执行中,直接跳过这一步。重写之后的Icon变得简单了很多。
1 2 3 4 5 6 7 8 var  loadIconsOnce sync.Oncevar  icons map [string ]image.Imagefunc  Icon (name string )     loadIconsOnce.Do(loadIcons)     return  icons[name] } 
竞险检测器 很多时候goroutine访问共享变量导致的竞险并不那么容易发现和避免。Go的配套工具链提供了-race标志位用来检查程序中可能存在的竞险情况,在go run, go build, go test后添加都可以。它可以记录对共享变量所做的所有读写操作以及对应的goroutine,还有程序中由sync和信道触发的同步操作。竞险检测器在分析这些事件流的时候可以给出包含共享变量和对其读写goroutine报告。在绝大多数情况下已经足够你查问题了。
竞险检测器只能报告程序覆盖到代码的竞险情况,所以尽量让测试覆盖到所有代码。竞险检查会稍微占用更多时间和内存,但是是可以接受的。
样例:并发无阻塞缓存 实现这么一个并发数据结构,大抵有两种构建思路:
不同场景下,他们实现的复杂度可能会稍有不同。
goroutine和线程 goroutine和线程有些很多小地方上的区别,这些区别让goroutine和线程有着较大区别。
线程的栈一般是固定的(通常是2MB),goroutine的栈是灵活的,从较少的大小开始(通常是2KB),可以扩大和缩小 
goroutine有自己的调度机制(m:n调度),把m个goroutine复用或调度到n的操作系统的线程 
GOMAXPROCS环境变量决定了Go代码可以使用多少个操作系统线程 
最后,goroutine没有其他操作系统或编程语言中用来支持多线程的为每个线程添加一个唯一标识的设计。这个是特别设计的,用来避免线程池的滥用。Go更推荐只由入参显式决定函数表现的风格,避免让函数收到执行线程的影响。
包和Go工具 如今中小型的程序可能会包含10000个函数,其中绝大多数来自其他人的包。包可以将程序中相互关联的特性整理到独立的单元中,进而在组织或社区中重用、分享。包名和导出的类型、变量、函数名都应简短清晰,Go使用首字母大小写控制可见性,从而掩盖实现细节,保证稳定性或实现互斥锁。
Go的构建速度算是比较快的。主要有3个原因:
Go在每个文件开头都显式列出了引入的文件,无需读取整个文件 
Go中没有引入是一个有向无环图,因此可以并行编译 
编译好的Go包的目标文件会包含自身和自身的依赖 ,每一次的import只需读取一遍目标文件即可 
 
import路径 Go的语言规范并没有规定import路径,路径的实现是由相关工具决定的。但是为了避免冲突,除了标准包以外的包,都需要以域名或组织名开头,如encoding/json,golang.org/x/net/html。
声明和import 每一个Go文件的开头,都需要以package xxx的形式标识包名。通常,包名是import路径的最后一段。但是有3个例外情况:
main包名表示告诉go build需要构建一个可执行文件目录下包含_test后缀文件名的,在执行go test时会额外被构建用于测试的包 
有的报名后面会跟版本号,这个时候以没有版本号的作为包名 
 
在import时,如果引入包较多时,可以用圆括号列表形式引入。列表间可以用空行隔开,通常用来分组域名,这个在按照字母顺序排序import时有用——排序会在每组内进行。如果不同域内的包名一样,如math/rand和crypto/rand,可以用重命名引入(renaming import)。像下面这样
1 2 3 4 import  (    "crypto/rand"      mrand "math/rand"  ) 
这种重命名只在当前文件内有效。重命名通常可以避免包名冲突,或者简化一些复杂的包名,在简化时,注意对同样的原名,使用同样的缩写名。go build会报告编译中循环依赖。
空导入 有的时候,我们的引入的目的在包的副作用 ,如其中全局变量的初始化或init函数的执行。这个时候可以用空白标识符_进行重命名即可,如import _ "image/png"。这种即空导入。
在书中例子里,对image/png的空导入,实现了png解码相关配置的全局注册(image.RegisterFormat),从而可以解码png图片。类似的思路在database/sql包中也有用到。
命名 对于包名,有下面一些建议的命名规范:
使用简短明了的包名 
使用有描述性且没有歧义的名字,且最好不要使用经常会用来命名局部变量的包名,如path 
包名通常使用单数形式,需要和其他情况区分开时,如strings,bytes 
避免使用具有隐藏含义的名称,如temp 
 
对于包中的成员名:
考虑和包名一起构成完整的含义,不需要包含包名,如flat.Int,http.Get 
有的包名表示一种类型,在内部会有对应的类型定义和New操作 
即使对于有着很多成员的包,其最关键的几个方法仍然是最简单的 
 
Go工具 Go工具像一个瑞士军刀,它的各个子命令提供了诸如包管理器(go get)、构建系统(go build, go run, go install)、测试驱动(go test)等等。
工作区组织 日常经常使用的是GOPATH环境变量,用于说明当前工作区的根路径。GOROOT表示go源码的根路径,GOOS表示操作系统,GOARCH表示处理器架构。更多配置可以执行go env查看。
包下载 执行go get下载,下载时不仅包含源码的拷贝,还包含源码的版本控制信息。Go工具会自动判断流行的代码托管方式。对于不那么有名的托管网站,需要自己显式说明保本控制的协议,可以通过go help importpath查看细节。
Go工具在访问包的导入路径域名如golang.org时,会试图从网页的<meta>标签中寻找类似下面这样指示目标路径的信息。
1 <meta  name ="go-import"  content ="golang.org/x/net git https://go.googlesource.com/net" > 
另外,执行go get -u时会获取所有包的最新版本,在需要锁定版本时比较麻烦,可以借助vendor 解决,在go help gopath中有介绍。
包构建 
使用go build构建时,对于库类型代码只会检查错误,对于main包,则会构建可执行文件 
一个目录包含一个包 ,因此导入时,要么指定导入路径,要么指定相对路径,否则会以当前目录为基础构建。构建得到的可执行文件名称是go文件的前缀。go build构建时,会丢弃已编译的代码,只保留可执行文件。go install构建时,会保留已编译的代码,编译好的包位于$GOPATH/pkg下,编译得到的执行文件位于$GOPATH/bin下。再之后,go build和go install不会编译未被改变的包或命令,从而让执行过程更快。go build -i可以安装项目的所有依赖修改GOOS和GOARCH可以改变包目标的平台和架构,默认只会针对当前平台和架构打包。 
文件末尾以诸如_linux.go,axm_amd64.s结尾时,只会在打对应平台包的时候才会编译此文件。另外还可以以// +build linux darwin的注释形式做到更好的细粒度。// +build ignore表示编译时跳过该文件。 
 
go doc go/build下有更多介绍。
包文档 Go建议在导出的包成员和包声明前使用描述用途和用法的注释。注释宜简单且清晰,对于大段的注释,使用同名的文档文件(通常名为doc.go)进行说明。如果行为本身就足够明显,就不需要写注释。
go doc命令可以查看包、包成员、包方法的文档。还有个很相似的命令godoc,它可以托管一个能够查看当前工作目录下文档的服务器。
内部包 有些包可能希望导出只对个别信任的包可见,对于这种包,导入路径中需要包含internal。这些内部包只对internal的父目录下文件可见,如net/http/internal/chunked对net/http/httputil可见,但对net/url不可见。
查询包 go list工具可以查询包的导入路径。使用...通配符可以查到更多内容。
1 2 3 4 go list github.com/go-sql-driver/mysql go list ... go list gopl.io/ch3/... go list ...xml... 
结合-json可以打印json格式的包详情,或者结合-f加上text/template语法打印特定格式的字符串。
更多使用方式查看go help list。
测试 同行评审和测试是两种避免代码错误的方式。Go尽量让写自动化测试代码不是一件很困难的事。在Go中进行测试,你需要了解的只不过是普通的Go语法规范和一些约定而已。
go test工具Go的测试都借助go test完成。所有和测试相关的文件必须以_test.go结尾,这些文件不会在打包时包括进去,只会在运行测试时运行。在文件中有三类函数会被特殊处理:
测试函数:必须以Test开头,表示检测一些逻辑的正确性,运行后会给出PASS或FAIL 
基准测试函数:必须以Benchmark开头,表示测量一些操作的性能,运行后会给出运行时间 
样例函数:必须以Example开头,表示提供一些格式化的文档 
 
go test运行完成时,会生成一个临时的main包,构建并运行,最后给出结果并清理现场
测试函数 测试函数均以Test开头,函数入参是test包提供的用来打印错误或其他日志的工具集。
1 2 3 import  "testing" func  TestSin (t *test.T) 
接着就像写普通Go代码一样去执行case就行了。
1 2 3 4 5 6 7 8 9 package  wordimport  "testing" func  TestPalindrome (t *testing.T)     if  !IsPalindrome("kayak" ) {         t.Error(`IsPalindrome("kayak") = false` )     } } 
运行时,结合-v标记可以打印详细信息,结合-run标识可以只运行符合指定模式的case。
1 go test  -v -run="French|Canal"   
case之间的代码相似性很高,建议用配置的方式批量运行case,减少模板代码书写。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func  TestPalindrome (t *tesing.T)     var  tests = []struct {         input string          want bool      } {         {"" , true },         {"a" , true },         {"ab" , false },         {"A man, a plan, a canal: Panama" : true }     }     for  _, test := range  tests {         if  got := IsPalindrome(test.input); got != test.want {             t.Errorf("IsPalindrome(%q) = %v" , test.input, got)         }     } } 
go test在运行测试时,每个case的错误不会中断其他case的执行,也不会panic,来保证一次运行能获得所有case的执行结果。对于需要中断的情况,可以使用t.Fatal或者t.Fatalf。
随机化测试 不同于上面提到的选择特定case进行测试。随机化测试可以覆盖更广的范围。在验证随机化测试是否符合预期上,有两种思路:
使用另外一种方式给出结果,对比两种方式的结果是否相同 
使用固定的模式生成随机化case,使其预期结果可以事先推导出来 
 
另外,在随机化测试时还要考虑能否再次复现问题case的情况。
测试一个命令 对于go test而言,main包只是一个库,可以将main函数中需要测试的逻辑抽离成函数,在*_test.go中测试即可。最好将log.Fatal或者os.Exit这种中断程序执行的语句放在main函数中,避免中断测试流程。
白盒测试 白盒测试即在对测试对象有清楚认识的情形下进行测试;黑盒测试则相反,更站在客户端的角度去测试包。在白盒测试下,我们可以修改原先包的一些实现方式,使之更易被测试。比如,可以将其中会有副作用的部分,如发邮件、写数据库、发短信的函数覆盖。(类似mock的思路)
但是,在覆盖后,别忘了还原回去,避免影响后续测试。类似下面这样:
1 2 3 4 5 6 7 8 9 10 func  TestCheckQuotaNotificationUser (t *testing.T)     saved := notifyUser     defer  func ()           notifyUser = func (user, msg string )          notifiedUser, notifiedMsg = user, msg     }      } 
这种覆盖方式正常情况下不会有风险,因为go test通常不会并行运行多个测试。
外部测试包 上面提到的都是直接在包下新建*_test.go文件的方式进行测试。有些情况下,如果测试文件内需要引用更高层包,会产生循环引用,这是上一章提到不允许的。这时可以定义为外部包。如:
net/url下的测试文件导入了net/http包,而net/http包中又导入了net/url。这个时候在net/url下的测试文件使用package url_test声明,表示是另一个包net/url_test。然后,通过导入net/url和net/http的方式进行测试。就可以避免循环引用。
可以通过go list -f指定.GoFiles,.TestGoFiles和.XTestGoFiles分别查看包中的源文件、测试文件和外部测试包文件。
然而,外部测试包并不能访问到包内对外不可见的变量或函数。这个时候,可以在包内创建一个后门测试文件,用于导出一些内部变量或函数对外部包测试可见,通常命名为export_test.go。这类文件内不包含实际的测试。如fmt包下的export_test.go。
1 2 3 package  fmtvar  IsSpace = isSpace
写高效的测试 Go在设计上和其他很多语言不同,并不包含一个大而全的测试框架,也没有创建、清除操作,和常用的断言、判断方法等。Go认为写case是作者自己的事,而且就像写普通的程序一样,不要有死记硬背和长篇大论,只需简明扼要地表达测试意图。
在写测试代码时,避免过早抽象,先想着把功能实现,然后再想怎么通过抽象减少重复和复杂度。
避免“脆弱”的测试 有两种应用:一种是真正bug很多的(buggy),另一种是合理改动也过不了case的(brittle)。而这里过不了case可能只是因为判断逻辑写的不够宽容,死抠细节导致很容易过时。避免这种情况一个很直接的办法是只检查你关心的特性,使用更简单和时间稳定的方式检查,如不要依赖字符串匹配。去检查本质。
覆盖率 
Testing shows the presence, not the absence of bugs —— Edsger Dijkstra
 
覆盖率一定程度上能对测试的覆盖程度有启发性的指示作用。使用go test -coverprofile可以指定覆盖率数据输出,如果不需要输出,只看摘要,可以只用go test -cover。使用go tool cover可以显示覆盖率使用介绍。
最后要说明的是,被覆盖到的代码并不是没有bug,测试是一种务实 的努力。它是在写测试代价和失败代价的中间的一个折中。
性能测试函数 这类函数都以Benchmark开头,和测试函数类似,函数入参是*testing.B类型的变量。默认情况下,不会执行任何性能测试,需要指定-bench值,去匹配对应函数执行,“.”表示匹配所有。如go test -bench=.性能测试函数写法如下:
1 2 3 4 5 6 import  "testing" func  BenchmarkIsPalindrome (b *testing.B)     for  i := 0 ; i < b.N; i++ {         IsPalindrome("A man, a plan, a canal: Panama" )     } } 
之所以需要自己在基准测试函数中写循环,而不集成在测试驱动中,是避免一些一次性操作影响执行时间测量。-benchmem标识会显示内存分配的使用情况。性能测试函数可以用来对比两种策略或算法的相对时间优劣,以及通过调整循环次数,整体上考察代码设计。
性能侧写(Profilling) 性能测试函数能帮你发现整体的性能好坏,但不能告诉你哪里做得不够好。
Knuth曾说过“不要过早优化”,然而结合上下文的原话的意思则是,寻找性能优化点并不那么容易,程序员们在写需求前浪费了大量时间在寻找优化点上,先把事情做出来,不要杞人忧天过早优化。但是优秀的程序员会努力找到优化点并改善之。
寻找关键点的方式就叫profiling 。profile通过采样的方式给出占用时间、资源最多的对象,从而可以对应去优化。Go提供3种profile
CPU profile,标记占用CPU时间最长的函数 
heap profile,标记分配内存最多的声明 
blocking profile,标记阻塞goroutine时间最久的操作 
 
对应在go test上的标识为-cpuprofile,-memprofile,-blockprofile。借助go tool pprof可以打印侧写数据,以及可视化数据。
样例函数 1 2 3 4 5 6 7 func  ExampleIsPalindrome ()     fmt.Println(IsPalindrome("A man, a plan, a canal: Panama" ))     fmt.Println(IsPalindrome("palindrome" ))                } 
最后一种会被go test特殊处理的是样例函数,这类函数以Example开头,并没有入参,也没有返回。它的作用主要有以下3点:
文档记录,且更能传达意图,同时由于样例函数是实际的Go代码,对比文档,随着代码演化,不会有过期风险。命名单纯叫Example的函数作为整个包的样例函数。 
函数最火包含// Output:注释的话,go test会检查标准输出是否能匹配注释中的输出 
在godoc中可以作为playground,提供给用户动态编辑、运行的功能 
 
反射 反射能在运行时 不知道变量类型情况下去修改和查询变量值。反射还能让我们将类型作为第一成员的值来使用。类似fmt.Sprintf和text/template中就有用到这个特性
reflect.Type和reflect.Valuereflect.Type和reflect.Value分别表示变量的类型和值。其中类型通过reflect.TypeOf得到,得到的reflect.Type可以保存任何类型值。
1 2 3 t := reflect.TypeOf(3 )  fmt.Println(t.String())  fmt.Println(t)  
返回的类型总是interface的动态类型,所以总是确切类型。
reflect.ValueOf可以得到任意类型的变量值。返回的reflect.Value满足fmt.Stringer接口,不过打印出来的是变量类型。
1 2 3 4 v := reflect.ValueOf(3 )  fmt.Println(v)  fmt.Printf("%v\n" , v)  fmt.Println(v.String())  
reflect.Value.Interface方法返回一个保存相同值的interface{}类型。它和reflect.Value不同在于,一个interface{}类型的变量掩盖了外部表现和内部实现细节,因此无从对其操作。``reflect.Value的Kind`方法可以返回类型的底层表示方法,因此使用时,可以只关心Go中定义的类型。
递归值输出函数Display 利用上面提到的Kind方法,可以实现递归打印任意类型值的函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 func  display (path string , v reflect.Value)     switch  v.Kind(){     case  reflect.Invalid:         fmt.Printf("%s = invalid\n" , path)     case  reflect.Slice, reflect.Array:         for  i := 0 ; i < v.Len(); i++ {             display(fmt.Sprintf("%s[%d]" , path, i), v.Index(i))         }     case  reflect.Struct:         for  i := 0 ; i < v.NumField(); i++ {             fieldPath := fmt.Sprintf("%s.%s" , path, v.Type().Field(i).Name)             display(fieldPath, v.Field(i))         }     case  reflect.Map:         for  _, key := range  v.MapKeys() {             display(fmt.Sprintf("%s[%s]" , path, formatAtom(key), v.MapIndex(key)))         }     case  reflect.Ptr:         if  v.IsNil() {             fmt.Printf("%s = nil\n" , path)         } else  {             display(fmt.Sprintf("(*%s)" , path), v.Elem())         }     case  reflect.Interface:         if  v.IsNil() {             fmt.Printf("%s = nil\n" , path)         } else  {             fmt.Printf("%s.type = %s\n" , path, v.Elem().Type())             display(path+".value" , v.Elem())         }     default :          fmt.Printf("%s = %s\n" , path, formatAtom(v))     } } 
上面用到了许多reflect.Value的方法,不是所有的都安全:
v.Index()和v.Len()类似len()和[i]下标取值v.NumbField()返回结构体中的字段数目,v.Field(i)则返回第i位的reflect.Value类型值v.MapKeys()返回无序的map keyv.IsNil()和v.Elem()分别判断是否为空和获取值 
上述方法在遇到有环的数据结构时,会无限打印,可以借助下一章里的unsafe包解决。
使用reflect.Value设置变量 Go中的变量都是有地址的,可以通过这个地址去修改变量的值。
1 2 3 4 5 x := 2  a := reflect.ValueOf(2 ) b := reflect.ValueOf(x) c := reflect.ValueOf(&x) d := c.Elem()  
上面的d即变量x。借助这个方式我们可以用Addr()获取地址,用Interface()获取interface{}类型的值,再使用类型断言转成具体的变量类型。像下面这样。
1 2 3 px := d.Addr().Interface().(*int ) *px = 3  fmt.Println(x)  
又或者,可以通过Set方法设置一个reflect.Value。针对特定类型,还有SetInt、SetUint、SetString这样的方法。注意,这些方法只使用在特定类型上,对于interface{}或其他类型使用,会引起panic。
1 2 3 4 5 6 d.Set(reflect.ValueOf(4 )) var  y interface {}ry := reflect.ValueOf(&y).Elem() ry.SetInt(2 )  ry.SetInt(reflect.Value(3 ))  
另外,反射不能更新那些没有对外导出的结构体字段,尽管这些字段可以在发射中读取到。CanSet()可以判断一个reflect.Value是否可以修改,类似的,CanAddr()可以判断一个reflect.Value是否可以获取到地址。
利用上面的特性,可以实现encoding/json中类似的解析JSON字符串的效果。
访问结构体的field tag 1 2 3 4 5 var  data struct  {    Labels []string  `http:"l"`      MaxResults int  `http:"max"`      Exact bool  `http:"x"`  } 
我们在JSON一节提到,可以在结构体后使用field tag 作为JSON解析过程中的metadata。实际上,除了json还可以设置其他tag。这个tag也可以通过反射特性拿到。
reflect.Type的Field()方法可以返回一个reflect.StructField类型,其中包含了字段名、字段类型以及可选的标签。其中Tag字段即field tag对应的字符串,它的Get方法可以返回特定标识后的标签值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 func  Unpack (req *http.Request, ptr interface {}) error  {    if  err := req.ParseForm(); err != nil  {         return  err     }     fields := make (map [string ]reflect.Value)     v := reflect.ValueOf(ptr).Elem()     for  i := 0 ; i < v.NumField(); i++ {         fieldInfo := v.Type().Field(i)          tag := fieldInfo.Tag          name := tag.Get("http" )          if  name == ""  {             name = strings.ToLower(fieldInfo.Name)         }         field[name] = v.Field(i)     }      } 
展示类型的方法 reflect.Type和reflect.Value都有一个Method()方法。reflect.Type中的方法返回reflect.Method实例,结构体中包含方法名和方法类型。reflect.Value中的Method()方法则返回一个reflect.Value类型,即一个绑定到receiver上的方法。
1 2 3 4 5 6 7 8 9 10 func  Print (x interface {})     v := reflect.ValueOf(x)     t := v.Type()     fmt.Println("type %s\n" , t)     for  i := 0 ; i < v.NumMethod(); i++ {         methType := v.Method(i).Type()         fmt.Printf("func (%s) %s%s\n" , t, t.Method(i).Name, strings.TrimPrefix(methType.String(), "func" ))     } } 
一些忠告 反射在规范的类型系统外,引入了更高自由度和编程的灵活性,但同时也带来了弱类型解释型语言(没错,JS就是你)的弊病:编译期问题会变成运行时问题、代码可读性变差、性能更差。
反射虽然提供了很强大的功能,但是失去了类型的保护,需要额外处理类型的边界case,否则很容易在运行时出现panic。而这些在使用特定类型时会在编译期就被发现。因此,在使用时,建议将包中使用反射的部分完全封装在内,不对外暴露,同时做一些额外的动态检查。同时,在出错时,给出类型上更友好的提示。
1 fmt.Printf("%d %s\n" , "hello" , 42 )  
另外,interface{}类型和大量出现的反射代码会让代码安逸理解,需要辅以更加完善的文档和注释来解释。
最后,基于反射的函数执行速度比普通基于特定类型的函数慢至少一两个级别。因此,尽量不要在代码执行的关键路径上使用反射实现,类似测试代码这种小数据量和执行覆盖频率的代码就可以使用。
低阶特性 Go已经尽量掩盖了它在底层的实现,用来避免出现难以调试的神秘问题。但在有些时候,比如为了追求性能,或者希望和操作系统底层交互,可能希望绕开这个限制。这一章的内容介绍的unsafe包提供了这么一个窗口,cgo工具可以将创建C库和Go的绑定关系。
unsafe.Sizeof,unsafe.Alignof和unsafe.Offsetof这三个API能让你了解一些Go在内存结构上的一些细节。其中
Sizeof返回操作数在内存中占用的大小Alignof返回操作数“对齐”需要的内存大小Offsetof返回结构体中字段在结构体内存的偏移量 
这几个API并不像它们名字里写的不安全,对于了解底层的内存表示是有帮助的,比如在需要优化内存性能时。
unsafe.Pointerunsafe.Pointer是一个可以指向任意类型变量的指针,同时也可以把unsafe.Pointer类型指针转换回特定类型指针 。
1 2 3 4 package  mathfunc  Float64bits (f float64 ) uint64  { return  *(*uint64 )(unsafe.Pointer(&f)) }fmt.Printf("%#016x\n" , Float64bits(1.0 ))  
同时unsafe.Pointer可以转换为uintptr类型,这个类型用整数表示了地址。这个整数类型足够大,足以表示任何类型的指针。但在使用时要多加注意,因为Go的垃圾回收机制使得一个变量的地址很可能会在运行过程中改变,从而使之前的uintptr类型变量失去意义。建议尽可能减少unsafe.Pointer到uintptr和对uintptr的使用。如果有包返回了一个uintptr类型,建议立即将其转换为unsafe.Pointer类型,确保指针能指向同一个变量。
cgo 使用cgo可以在go中使用C语言,反之亦然,这里从略,具体参考https://golang.org/cmd/cgo。 
再一些忠告 unsafe包和reflect包很像,提供了一些高级特性,但是更甚。它绕开了语言对不可控因素的隔离,会带来一些风险。所以,在特殊场景下,经过仔细考量和验证证实,使用unsafe确实会带来关键性能提升时,再在代码的关键位置使用unsafe,同时,尽量保证对代码其他地方透明。
最后,忘掉最后两章吧,先去踏踏实实写一些Go程序,在能用上reflect和unsafe的时候,你自然回想起来的。
祝,happy Go programming。
-END-