《Go语言圣经》电子书
《The Go Programming Language》源码
之前看了《go by example》似乎有必要找一本go的书看一下。
目录
第一章 入门
Hello, World
Go语言不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。实际上,编译器会主动把特定符号后的换行符转换为分号,因此换行符添加的位置会影响Go代码的正确解析。
- 函数的左括号{必须和func函数声明在同一行上,且位于末尾,不能独占一行。
- 而在表达式
x + y
中,可在+后换行,不能在+前换行(译注:以+结尾的话不会被插入分号分隔符,但是以x结尾的话则会被分号分隔符,从而导致编译错误)。
命令行参数
自增语句
i++
给i加1;这和i += 1
以及i = i + 1
都是等价的。对应的还有i--
给i减1。它们是语句,而不像C系的其它语言那样是表达式。
- 所以
j = i++
非法,而且++
和--
都只能放在变量名后面,因此--i
也非法。
func main() {
s, sep := "", ""
for _, arg := range os.Args[1:] {
s += sep + arg
sep = " "
}
fmt.Println(s)
}
在s += sep + arg
中,每次每次循环迭代字符串s的内容都会更新。+=连接原字符串、空格和下个参数,产生新字符串,并把它赋值给s。s原来的内容已经不再使用,将在适当时机对它进行垃圾回收。
第二章 程序结构
在Go语言中,返回函数中局部变量的地址也是安全的。例如下面的代码,调用f函数时创建局部变量v,在局部变量地址被返回之后依然有效,因为指针p依然引用这个变量。
var p = f()
func f() *int {
v := 1
return &v
}
编译器会自动选择在栈上还是在堆上分配局部变量的存储空间,但可能令人惊讶的是,这个选择并不是由用var还是new声明变量的方式决定的。
func f() {
var x int
x = 1
global = &x
}
func g() {
y := new(int)
*y = 1
}
f函数里的x变量必须在堆上分配,因为它在函数退出后依然可以通过包一级的global变量找到,虽然它是在函数内部定义的;用Go语言的术语说,这个x局部变量从函数f中逃逸了。相反,当g函数返回时,变量*y将是不可达的,也就是说可以马上被回收的。因此,y并没有从函数g中逃逸,编译器可以选择在栈上分配y的存储空间(译注:也可以选择在堆上分配,然后由Go语言的GC回收这个变量的内存空间),虽然这里用的是new方式。其实在任何时候,你并不需为了编写正确的代码而要考虑变量的逃逸行为,要记住的是,逃逸的变量需要额外分配内存,同时对性能的优化可能会产生细微的影响。
第三章 基础数据类型
Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。本章介绍基础类型,包括:数字、字符串和布尔型。复合数据类型——数组和结构体——是通过组合简单类型,来表达更加复杂的数据结构。引用类型包括指针、切片、字典、函数、通道,虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝。
3.5. 字符串
一个字符串是一个不可改变的字节序列。
3.6. 常量
- iota的用法
type Weekday int
const (
Sunday Weekday = iota //0
Monday //1
Tuesday //2
Wednesday //3
Thursday //4
Friday //5
Saturday //6
)
type Flags uint
const (
FlagUp Flags = 1 << iota // 1 is up
FlagBroadcast // 2 supports broadcast access capability
FlagLoopback // 4 is a loopback interface
FlagPointToPoint // 8 belongs to a point-to-point link
FlagMulticast // 16 supports multicast access capability
)
- 关于常量的类型,与类型转换,如下例子
const a float32 = 1.1111111111111111
const b = 1.1111111111111111
var t1 float64 = a // panic:cannot use a (type float32) as type float64 in assignment
var t2 float64 = float64(a)
var t3 float64 = b
//---------------------------------------------------
var f float64 = 212
fmt.Println((f - 32) * 5 / 9) // "100"; (f - 32) * 5 is a float64
fmt.Println(5 / 9 * (f - 32)) // "0"; 5/9 is an untyped integer, 0
fmt.Println(5.0 / 9.0 * (f - 32)) // "100"; 5.0/9.0 is an untyped float
第四章 复合数据类型
4.1. 数组
- 定义数组可以用如下方式
var q [3]int = [3]int{1, 2, 3}
var r [3]int = [3]int{1, 2}
//b长度为2
b := [...]int{1, 2}
//定义了一个含有100个元素的数组r,最后一个元素被初始化为-1,其它元素都是用0初始化。
r := [...]int{99: -1}
- 对函数来说,对数组参数的任何的修改都是发生在复制的数组上,并不能直接修改调用时原始的数组变量。如下例子
func zero(ptr *[32]byte) { //将数组ptr清零,传递的时候也要传递数组的引用
for i := range ptr {
ptr[i] = 0
}
}
4.2. Slice
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。一个slice由三个部分构成:指针、长度和容量。指针指向第一个slice元素对应的底层数组元素的地址
months := [...]string{1:"January", 12:"December"} //数组
fmt.Printf("%T\n", months) // [13]string
fmt.Printf("%T", months[:]) // []string
- 如果切片操作超出cap(nums )的上限将导致一个panic异常,但是超出len(nums)则是意味着扩展了slice,因为新slice的长度会变大:
nums := [...]int{1, 2, 3, 4, 5, 6, 7}
slice1 := nums[1:3] // [2 3]
slice2 := nums[1:10] // invalid slice index 10 (out of bounds for 7-element array)
slice3 := slice1[:4] // [2 3 4 5]
- 因为slice值包含指向第一个slice元素的指针,因此向函数传递slice将允许在函数内部修改底层数组的元素。换句话说,复制一个slice只是对底层的数组创建了一个新的slice别名。
var nums []int
nums = append(nums, 1)
nums1 := nums
nums[0] = 3 //修改nums也会修改nums1
- 测试一个slice是否是空的,使用
len(s) == 0
来判断,而不应该用s == nil
来判断。
var s []int // len(s) == 0, s == nil
s = nil // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil
s = []int{} // len(s) == 0, s != nil
- 在底层,make创建了一个匿名的数组变量,然后返回一个slice;只有通过返回的slice才能引用底层匿名的数组变量。在第一种语句中,slice是整个数组的view。在第二个语句中,slice只引用了底层数组的前len个元素,但是容量将包含整个的数组。额外的元素是留给未来的增长用的。
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
4.2.1. append函数
一
func appendInt(x []int, y int) []int {
var z []int
zlen := len(x) + 1
if zlen <= cap(x) {
// There is room to grow. Extend the slice.
z = x[:zlen]
} else {
// There is insufficient space. Allocate a new array.
// Grow by doubling, for amortized linear complexity.
zcap := zlen
if zcap < 2*len(x) {
zcap = 2 * len(x)
}
z = make([]int, zlen, zcap)
copy(z, x) // a built-in function; see text
}
z[len(x)] = y
return z
}
func main() {
nums1 := []int{1,2,3}
fmt.Println(len(nums1), cap(nums1)) // 3 3
nums2 := appendInt(nums1, 1)
fmt.Println(len(nums2), cap(nums2)) // 4 6
nums3 := appendInt(nums2, 1)
fmt.Println(len(nums3), cap(nums3)) // 5 6
//nums2与nums3引用同一个底层数组,即len(nums2) < len(nums3) 但是cap(nums2) == cap(nums2)
nums1[0] = 100
fmt.Println(nums1, nums2, nums3) // [100 2 3] [1 2 3 1] [1 2 3 1 1]
nums2[0] = 200
fmt.Println(nums1, nums2, nums3) // [100 2 3] [200 2 3 1] [200 2 3 1 1]
}
4.3. Map
- 创建map的方法,只要是可以通过
==
比较的类型就可以作为key
ages := make(map[string]int) // mapping from strings to ints
ages := map[string]int{
"alice": 31,
"charlie": 34,
}
-
删除map中元素的方法。
delete(ages, "alice") // remove element ages["alice"]
-
禁止对map元素取址的原因是map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。
_ = &ages["bob"] // compile error: cannot take address of map element
-
Map的value类型也可以是一个聚合类型,比如是一个map或slice。其中addEdge函数惰性初始化map是一个惯用方式,也就是说在每个值首次作为key时才初始化。addEdge函数显示了如何让map的零值也能正常工作;即使from到to的边不存在,graph[from][to]依然可以返回一个有意义的结果。
var graph = make(map[string]map[string]bool)
func addEdge(from, to string) {
edges := graph[from]
if edges == nil {
edges = make(map[string]bool)
graph[from] = edges
}
edges[to] = true
}
func hasEdge(from, to string) bool {
return graph[from][to]
}
4.4. 结构体
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,那样的话两个结构体将可以使用
==
或!=
运算符进行比较。相等比较运算符==
将比较两个结构体的每个成员。可比较的结构体类型和其他可比较的类型一样,可以用于map的key类型。
- 结构体的申明定义的三种方式
type Node struct {
a int
b int
}
func main() {
var node1 Node
node1.a = 1
node1.b = 2
node2 := Node{1, 2}
node3 := Node{a : 1, b : 2}
fmt.Println(node1, node2, node3)
}