Go 语言圣经学习笔记

2018年11月08日

命令行参数

程序的命令行参数可从 os.Args 中获取,os.Args 变量是一个字符串切片。os.Args 第一个元素是命令本身的名字,其余为传入的参数,因此参数列表可写为 os.Args[1:]

字符串操作

1import strings
2strings.Join(os.Args[1:], " ")
3
4strings.Split(data, "\n")

读取 stdin

1import "bufio"
2import "os"
3input := bufio.NewScanner(os.Stdin)
4for input.Scan() {
5    input.Text()
6}

printf

%d          十进制整数
%x, %o, %b  十六进制,八进制,二进制整数。
%f, %g, %e  浮点数: 3.141593 3.141592653589793 3.141593e+00
%t          布尔:true或false
%c          字符(rune) (Unicode码点)
%s          字符串
%q          带双引号的字符串"abc"或带单引号的字符'c'
%v          变量的自然形式(natural format)
%T          变量的类型
%%          字面上的百分号标志(无操作数)

os

1f, err := os.Open(filename)
2input := bufio.NewScanner(f)
3f.Close()

读取文件

1import "io/ioutil"
2data, err := ioutil.ReadFile(filename)

http

1resp, err := http.Get(url)
2b, err := ioutil.ReadAll(resp.Body)
3resp.Body.Close()
4fmt.Printf("%s", b)

time

1import "time"
2start := time.Now()
3secs := time.Since(start).Seconds()

关键字

break      default       func     interface   select
case       defer         go       map         struct
chan       else          goto     package     switch
const      fallthrough   if       range       type
continue   for           import   return      var

预定义

内建常量: true false iota nil

内建类型: int int8 int16 int32 int64
          uint uint8 uint16 uint32 uint64 uintptr
          float32 float64 complex128 complex64
          bool byte rune string error

内建函数: make len cap new append copy close delete
          complex real imag
          panic recover

访问

如果一个名字是在函数内部定义,那么它就只在函数内部有效。如果是在函数外部定义,那么将在当前包的所有文件中都可以访问。名字的开头字母的大小写决定了名字在包外的可见性。如果一个名字是大写字母开头的(译注:必须是在函数外部定义的包级名字;包级函数名本身也是包级名字),那么它将是导出的,也就是说可以被外部的包访问,例如 fmt 包的 Printf 函数就是导出的,可以在 fmt 包外部访问。包本身的名字一般总是用小写字母。

一个 Go 语言编写的程序对应一个或多个以.go 为文件后缀名的源文件。每个源文件中以包的声明语句开始,说明该源文件是属于哪个包。包声明语句之后是 import 语句导入依赖的其它包,然后是包一级的类型、变量、常量、函数的声明语句,包一级的各种类型的声明语句的顺序无关紧要(译注:函数内部的名字则必须先声明之后才能使用)

包级别的名字,例如在一个文件声明的类型和常量,在同一个包的其他源文件也是可以直接访问的,就好像所有代码都在一个文件一样。

字符串

\a      响铃
\b      退格
\f      换页
\n      换行
\r      回车
\t      制表符
\v      垂直制表符
\'      单引号 (只用在 '\'' 形式的rune符号面值中)
\"      双引号 (只用在 "..." 形式的字符串面值中)
\\      反斜杠

原生字符串,无转移操作

1const GoUsage = `Go is a tool for managing Go source code.
2
3Usage:
4    go command [arguments]
5...`

字符串和字节 slice 相互转换

1s := "abc"
2b := []byte(s)
3s2 := string(b)

strings 包提供的函数

1func Contains(s, substr string) bool
2func Count(s, sep string) int
3func Fields(s string) []string
4func HasPrefix(s, prefix string) bool
5func Index(s, sep string) int
6func Join(a []string, sep string) string

bytes 包提供的函数

1func Contains(b, subslice []byte) bool
2func Count(s, sep []byte) int
3func Fields(s []byte) [][]byte
4func HasPrefix(s, prefix []byte) bool
5func Index(s, sep []byte) int
6func Join(s [][]byte, sep []byte) []byte

strconv 包提供字符串和数字转换的功能

1x := 123
2y := fmt.Sprintf("%d", x)
3fmt.Println(y, strconv.Itoa(x)) // "123 123"
4
5x, err := strconv.Atoi("123") // x is an int

常量

常量表达式的值在编译期计算。

批量声明

1const (
2    a = 1
3    b
4    c = 2
5    d
6)
7fmt.Println(a, b, c, d) // "1 1 2 2"

iota 常量生成器,在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加一。

 1type Weekday int
 2const (
 3    Suncay Weekday = iota
 4    Monday
 5    Tuesday
 6    Wednesday
 7    Thursday
 8    Friday
 9    Saturday
10)

数组

在数组字面值中,如果在数组的长度位置出现 ... 省略号,则表示数组的长度是根据初始化值的个数来计算的

1q := [...]int{1, 2, 3}
2
3// 长度为100
4r := [...]int{99: -1}
 1// push v
 2stack = append(stack, v)
 3// top of stack
 4top := stack[len(stack)-1]
 5// pop
 6stack = stack[:len(stack)-1]
 7
 8// remove
 9func remove(slice []int, i int) []int {
10    copy(slice[i:], slice(i+1:]
11    return slice[:len(slice)-1]
12}

Map

Map 的迭代顺序是随机的,如需按照顺序遍历,需通过 sort 排序

 1import "sort"
 2
 3names := make([]string, 0, len(args))
 4for names := range args {
 5    names = append(names, name)
 6}
 7sort.Strings(names)
 8for _, name := range names {
 9    fmt.Printf("%s\t%d\n", name, args[name])
10}

JSON

 1import "encoding/json"
 2
 3type Movie struct {
 4	Title  string `json:"title"`
 5	Year   int    `json:"released"`
 6	Color  bool   `json:"color,omitempty"`
 7	Actors []string
 8}
 9
10data, err := json.Marshal(movies)
11data, err = json.MarshalIndent(movies, "", "  ")
12
13
14var items []Movie
15if err := json.Unmarshal(data, &items); err != nil {
16    log.Fatalf("JSON unmarshaling failed: %s", err)
17}
18
19var result IssuesSearchResult
20	if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
21		resp.Body.Close()
22		return nil, err
23	}
24	resp.Body.Close()

函数

实参通过值的方式传递,因此函数的形参是实参的拷贝。对形参进行修改不会影响实参。但是,如果实参包括引用类型,如指针,slice(切片)、map、function、channel等类型,实参可能会由于函数的间接引用被修改。

在 Go 中,函数被看作第一类值(first-class values):函数像其他值一样,拥有类型,可以被赋值给其他变量,传递给函数,从函数返回。对函数值(function value)的调用类似函数调用。

拥有函数名的函数只能在包级语法块中被声明,通过函数字面量(function literal),我们可绕过这一限制,在任何表达式中表示一个函数值。函数字面量的语法和函数声明相似,区别在于 func 关键字后没有函数名。函数值字面量是一种表达式,它的值被成为匿名函数(anonymous function)。

参数数量可变的函数称为可变参数函数。

 1func sum(vals ...int) int {
 2	total := 0
 3	for _, val := range vals {
 4		total += val
 5	}
 6	return total
 7}
 8
 9fmt.Println(sum(1, 2, 3))
10
11values := []int{1, 2, 3, 4}
12fmt.Println(sum(values...))

当 defer 语句被执行时,跟在 defer 后面的函数会被延迟执行。直到包含该 defer 语句的函数执行完毕时,defer 后的函数才会被执行,不论包含 defer 语句的函数是通过 return 正常结束,还是由于 panic 导致的异常结束。你可以在一个函数中执行多条 defer 语句,它们的执行顺序与声明顺序相反。

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

panic

1panic(p)
2p := recover()

嵌入结构体扩展类型

1import "image/color"
2
3type Point struct{ X, Y float64 }
4
5type ColoredPoint struct {
6    Point
7    Color color.RGBA
8}
 1var cache = struct {
 2	sync.Mutex
 3	mapping map[string]string
 4}{
 5	mapping: make(map[string]string),
 6}
 7
 8func Lookup(key string) string {
 9	cache.Lock()
10	v := cache.mapping[key]
11	cache.Unlock()
12	return v
13}

接口

接口类型是一种抽象的类型。接口类型具体描述了一系列方法的集合,一个实现了这些方法的具体类型是这个接口类型的实例。

一个类型如果拥有一个接口需要的所有方法,那么这个类型就实现了这个接口。

概念上讲一个接口的值,接口值,由两个部分组成,一个具体的类型和那个类型的值。它们被称为接口的动态类型和动态值。对于像 Go 语言这种静态类型的语言,类型是编译期的概念;因此一个类型不是一个值。在我们的概念模型中,一些提供每个类型信息的值被称为类型描述符,比如类型的名称和方法。在一个接口值中,类型部分代表与之相关类型的描述符。

sort

1import "sort"
2
3names := []string{"b", "c", "a", "e", "d"}
4
5sort.Strings(names)
6
7sort.Sort(sort.StringSlice(names))
8
9sort.Sort(sort.Reverse(sort.StringSlice(names)))

Channels

无缓存

一个基于无缓存 Channels 的发送操作将导致发送者 goroutine 阻塞,直到另一个 goroutine 在相同的 Channels 上执行接收操作,当发送的值通过 Channels 成功传输之后,两个 goroutine 可以继续执行后面的语句。反之,如果接收操作先发生,那么接收者 goroutine 也将阻塞,直到有另一个 goroutine 在相同的 Channels 上执行发送操作。

基于无缓存 Channels 的发送和接收操作将导致两个 goroutine 做一次同步操作。因为这个原因,无缓存 Channels 有时候也被称为同步 Channels。当通过一个无缓存 Channels 发送数据时,接收者收到数据发生在唤醒发送者 goroutine 之前(译注:happens before,这是 Go 语言并发内存模型的一个关键术语!)。

在讨论并发编程时,当我们说 x 事件在 y 事件之前发生(happens before),我们并不是说 x 事件在时间上比 y 时间更早;我们要表达的意思是要保证在此之前的事件都已经完成了,例如在此之前的更新某些变量的操作已经完成,你可以放心依赖这些已完成的事件了。

Go 语言的类型系统提供了单方向的 channel 类型,分别用于只发送或只接收的 channel。类型 chan<- int 表示一个只发送 int 的 channel,只能发送不能接收。相反,类型<-chan int 表示一个只接收 int 的 channel,只能接收不能发送。(箭头<-和关键字 chan 的相对位置表明了 channel 的方向。)这种限制将在编译期检测。

有缓存

向缓存 Channel 的发送操作就是向内部缓存队列的尾部插入元素,接收操作则是从队列的头部删除元素。如果内部缓存队列是满的,那么发送操作将阻塞直到因另一个 goroutine 执行接收操作而释放了新的队列空间。相反,如果 channel 是空的,接收操作将阻塞直到有另一个 goroutine 执行发送操作而向队列插入元素。

1cap(ch) // channel内部缓存的容量
2len(ch) // channel内部缓存队列中有效元素的个数

竞态

不要使用共享数据来通信;使用通信来共享数据

导入两个同名包。导入包的重命名只影响当前的源文件。其它的源文件如果导入了相同的包,可以用导入包原本默认的名字或重命名为另一个完全不同的名字。

1import (
2    "crypto/rand"
3    mrand "math/rand" // alternative name mrand avoids conflict
4)

如果只是导入一个包而并不使用导入的包将会导致一个编译错误。但是有时候我们只是想利用导入包而产生的副作用:它会计算包级变量的初始化表达式和执行导入包的 init 初始化函数。这时候我们需要抑制“unused import”编译错误,我们可以用下划线来重命名导入的包。像往常一样,下划线为空白标识符,并不能被访问。

1import _ "image/png" // register PNG decoder

这个被称为包的匿名导入。

工作区结构

对于大多数的 Go 语言用户,只需要配置一个名叫 GOPATH 的环境变量,用来指定当前工作目录即可。当需要切换到不同工作区的时候,只要更新 GOPATH 就可以了。

GOPATH 对应的工作区目录有三个子目录。其中 src 子目录用于存储源代码。其中 pkg 子目录用于保存编译后的包的目标文件,bin 子目录用于保存编译后的可执行程序。

测试

go test 命令是一个按照一定的约定和组织来测试代码的程序。在包目录内,所有以_test.go 为后缀名的源文件在执行 go build 时不会被构建成包的一部分,它们是 go test 测试的一部分。

在_test.go 文件中,有三种类型的函数:测试函数、基准测试(benchmark)函数、示例函数。一个测试函数是以 Test 为函数名前缀的函数,用于测试程序的一些逻辑行为是否正确;go test 命令会调用这些测试函数并报告测试结果是 PASS 或 FAIL。基准测试函数是以 Benchmark 为函数名前缀的函数,它们用于衡量一些函数的性能;go test 命令会多次运行基准函数以计算一个平均的执行时间。示例函数是以 Example 为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。