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为函数名前缀的函数,提供一个由编译器保证正确性的示例文档。