教程
- A Tour of Go
- Go by Example
- Go入门指南 / Github
- Go 语言程序设计
- 怎么学习golang? 里面给出了一些不错的入门资料
- Go Wiki
笔记
2017-01-01: 目前是官网入门指南 A Tour of Go 的笔记,大部分代码来至上面。
GOROOT 和 GOPATH
关于 GOROOT
和 GOPATH
环境变量,如果是系统默认安装,而非自定义的安装目录,则 GOROOT
不需要设置。
GOROOT must be set only when installing to a custom location. from
关于 GOPATH
,必须设置,在get/build/install包时用到,第三方的包都会装在这个目录下,包括里面的二进制文件,所以建议将 $GOPATH/bin
加入到 $PATH
环境变量中。更多可以 go help gopath
。
package 和 import
一个基本的例子:
package main import "fmt" func main() { fmt.Println("Hello, World") }
其中package我的理解是继承或者说基于的包名; main包表示这个程序的执行入口,编译时会将这个编译为可执行程序 (go build
) 而不是 $GOPATH/pkg/
下的静态库 (go install
)。类似Python中的 if __name__ == '__main__'
。
import 则表示导入要使用的标准库包或第三方包。
参考:
多个import语句可以使用 打包导入(factored import),更优雅:
import ( "fmt" "math" ) // 等价 import "fmt" import "match"
Exported names
在 Go 中,首字母大写的名称是 可被导出 的。当 import 包时,不被导出的包是无法被访问使用的,所以可以看到如上面的 fmt
包的 Println()
是以大写字母开头。
Function
func add(x, y int) int { return x + y } func swap(x, y string) (string, string) { return y, x }
函数名后面圆括号里接 参数。
参数后面指定 返回类型,多个相同的类型 不能 省略只写一个; 单个返回值可以不用圆括号括起来; 没有返回值则不写。
Named return values(命名返回值),即在函数名后的返回值指定变量名,函数体内配合裸 return 来返回,注意 return 后面不要接返回值了,否则命名返回无效,使用的还是返回的值。另外在这种情况下,返回类型如果相同是可以省略只写一个:
func swap(x, y string) (m, n string) { m, n = y, x return }
这里不能对参数做命名返回值返回,否则报错:
// 错误的 func swap(x, y string) (x, y string) { x, y = y, x return }
函数参数,闭包没啥好写的
Variable, Constants and Type
几种定义方式:
var i, j bool var x, y int = 1, 2 func main() { var m, n = "abc", 100 z := 200 fmt.Println(i, j, x, y, m, n, z) } // 输出: false false 1 2 abc 100 200
Go中声明(declaration)和定义(definition)个人理解是不做区分的(和C不一样),因为如果声明了变量但是未显式赋值,会隐式赋值给各类型的初始值(zero value,零值)。
另外 var var_name var_type
和函数参数一样,变量名在前,类型在后。
另外也可以不指定类型名,Go 会根据赋值判断相应类型
最后,也可以不写 var
,改用 :=
的简明赋值语句,但是此语法 只能用于函数内,而 var
则可以在函数外使用。
并且在至少有一个新变量,:=
可以用于重声明,比如下面的例子:
func main() { var a, b = 1, 2 fmt.Println(a, b) // b 是重声明的,c 是新变量 // 如果改为 a, b := 3, 4 则报错:no new variables on left side of := c, b := 3, 4 fmt.Println(c, b) }
声明和导入包一样,可以 打包声明:
var ( i bool = false j int = 1 z float64 = 0.3 )
关于类型关键词,其中 byte
是 uint8
的别名,rune
是 int32
的别名。
一般情况下,数字用 int
即可。
一些基本类型的零值:
- 数字: 0
- 布尔: false
- 字符串: ""
Go中类型的转换用 T(v)
,将值 v 转换为类型 T:
i := 1 f := float64(i)
Go中类型转换 必须 显示指定(C中是可以做隐式转换的)。
常量类型变量声明,使用关键字 const
,不能使用 :=
语法
const World = '世界' const ( Big = 1 << 100 Small = Big >> 99 )
for / if / switch
for
支持几种语法:
// init statement; condition expression; post statement // 另外这里注意只有后自增,没有前自增; 写C/C++时习惯了用前自增,这里总写错 for i := 1; i <= 10; i++ { fmt.Println(i) } // init / post statement 可以省略 sum := 1 for ; sum <= 10; { sum += sum } // 上面的例子,前后两个分号`;`也可以省略,这就是while的语法了 sum := 1 for sum <= 10 { sum += sum } // 上面的例子,退出条件也省略,就是无限循环了 for { }
if
语法:
// condition expression if i <= 10 { ... } // if也支持init statement,if初始化的变量作用域只在if主体内 if i := 5; i <= 10 { ... } // if ... else if ... else if i := 5; i <= 3 { fmt.Println("<= 3") } else if i <= 6 { fmt.Println("<= 6") } else { fmt.Println("> 6") }
switch
语法:
// 同样支持初始化语法 // 和C不同,每个case语句的行为是自动`break`,不需要手动写`break` // 如果想保持和C的行为一致,即匹配后还继续往下执行,则可以在case中加上`fallthrough` switch os := runtime.GOOS; os { case "darwin": fmt.Println("OS X.") case "linux": fmt.Println("Linux.") default: fmt.Printf("%s.", os) } // 如果switch condition没写,则默认表示 `true` // 行为和if ... else if ... else 一样 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.") }
注:
- Go中初始、条件等语句不需要用
()
阔起来 - 主体部分必须用花括号
{}
阔起来
defer
defer
语句用于延迟函数的执行直到当前函数 return,但是 defer 的参数会立刻生成。
多个defer语句会进行 压栈,最后执行时是 LIFO:
fmt.Println("begin") for i := 0; i < 3; i++ { defer fmt.Println(i) } fmt.Println("done") // 返回: begin -> end -> 2 -> 1 -> 0
Pointers
i := 100 var p1 *int // 如果没有初始化,则零值是`nil` p1 = &i p2 := &i *p2 = 101 fmt.Println(i, *p1, *p2)
*T
在声明时表示指向值T的指针&
用于对值 取址*p
在使用时表示对指针的 解引用,即取指针指向的值。
这里*
需要注意,在不同地方的含义不一样。
Struct
结构体用法:
type Vertex struct { X int Y int } var v Vertex = Vertex{3, 5} // 结构体初始化 v.X = 6 // 通过dot获取结构体字段 fmt.Println(v) p := &v // 结构体指针 (*p).X = 7 // 结构体指针获取结构体字段 fmt.Println(v) p.X = 8 // 上面的用法太笨拙,这个更简单 fmt.Println(v) // 输出: // {6 5} // {7 5} // {8 5} // Struct Literals 结构体字面值 v1 := Vertex{X: 1} // X: 1, Y: 0 v2 := Vertex{} // X: 0, Y: 0 p := &Vertex{1, 2} // has type *Vertex fmt.Println(v1, v2, p) // 输出: {1 0} {0 0} &{1 2} // 注意p的输出结构式带有`&`,表示输出的是结构体指针
Array
数组是定长的 [n]T
var a1 [2]string a1[0] = "hello" a1[1] = "world" a2 := [3]int{1, 2, 3} fmt.Println(a1, a2) // 输出: [hello world] [1 2 3]
注意长度 [n]
也是类型的一部分
Slice
数组是定长的,Go还提供了切片这个数据结构,长度是动态变化的,所以这个用的比数组更频繁。
(刚看到 slice/切片 这个词,第一反应是一个函数,结果是一个数据结构...)
因为长度是动态变化,所以声明是 []T
,括号中不写。
Go中做切片(这里是动词)操作,返回的是切片。
array := [3]int{1, 2, 3} var slice1 []int = array[1:3] slice2 := array[0:2] fmt.Println(slice1, slice2) // import "reflect" fmt.Println(reflect.TypeOf(slice1), reflect.TypeOf(slice2)) // 输出: // [2 3] [1] // []int []int
切片自身并不存储数据,它是对底层数组的引用。
所以对切片中数据的修改,会影响相应的底层数组的值,也会影响其它引用到这个数组的切片
// 接上面的例子 slice1[0] = 100 fmt.Println(array, slice1, slice2) // 输出: [1 100 3] [100 3] [1 100]
切片字面值(slice literal)和数组字面值(array literal)一样,只是不需要指定长度:
slice1 := []int{1, 2, 3} slice2 := []struct { i int b bool }{ {1, true}, {2, false}, {3, true}, // 注意最后的逗号不能省略 } fmt.Println(slice1, slice2) // 输出: [1 2 3] [{1 true} {2 false} {3 true}]
上面注意最后的逗号不能省略,否则报错:
missing ',' before newline in composite literal
原因参考 这个回答:
a semicolon is automatically inserted into the token stream at the end of a non-blank line if the line's final token is
- ...
- one of the operators and delimiters ++, --, ), ], or }
切片的使用和 Python 类似,支持:
s[0:10] s[:10] s[0:] s[:]
切片有 length 和 capacity 的概念
length
通过len(s)
获取,表示切片中元素的个数capacity
通过cap(s)
获取,表示切片引用的 底层数组 中元素的个数,从切片的第一个元素开始计算
下面这个例子比较有意思,感觉容易入坑:
func printSlice(s []int) { fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s) } func main() { s := []int{1, 2, 3, 4, 5} printSlice(s) s = s[:0] printSlice(s) s = s[:4] // 注意这里还可以扩展,因为是引用,底层数组一直存在 printSlice(s) s = s[2:5] // 这里capacity就减少了 printSlice(s) } // 输出 // len=5 cap=5 [1 2 3 4 5] // len=0 cap=5 [] // len=4 cap=5 [1 2 3 4] // len=3 cap=3 [3 4 5]
切片的零值是 nil
:
var s []int // 注意和s := []int{}不一样,这个是空切片,赋值过的 fmt.Println(s, len(s), cap(s)) if s == nil { fmt.Println("slice is nil") }
make
函数可以用来创建切片,并指定 length(必选)和 capacity(可选):
s1 := make([]int, 3) printSlice(s1) s2 := make([]int, 3, 5) printSlice(s2) // 输出 // len=3 cap=3 [0 0 0] // len=3 cap=5 [0 0 0]
二维切片(类似C中二维数组):
// len(s) = 2, cap(s) = 2 s := [][]string{ []string{"a", "b"}, []string{"c", "d"}, }
Go对切片的 append 操作提供了内置函数append(s []T, v1, v2, v3, ...T) []T
,最后返回append后的切片; 因为切片大小是动态的,所以如果capacity不够,会自动扩容:
s := make([]int, 1, 3) printSlice(s) s = append(s, 1) printSlice(s) s = append(s, 2, 3) printSlice(s) // 输出: // len=1 cap=3 [0] // len=2 cap=3 [0 1] // len=4 cap=6 [0 1 2 3]
for循环切片:
s := []int{1, 2, 3} // i 是切片索引,v是值的一个copy for i, v := range s { fmt.Println(i, v) } // i 是切片索引 for i := range s { fmt.Println(s[i]) }
和Python一样,如果不关心索引,可以直接赋值给变量名_
关于 range 的扩展阅读:
Maps
映射(也就是字典吧)表示一个 key/value 对集合,声明语法:
var map_name map[map_key_type]map_value_type
map_value_type
表示 map 值的类型,类似于 slice 的 []int
这种表示 slice 中的值是 int。
即:
type Vertex struct { X int Y int } var m map[string]Vertex // 声明 func main() { m = make(map[string]Vertex) // 创建 m["a"] = Vertex{3, 4} m["b"] = Vertex{1, 2} fmt.Println(m) } // 输出: map[a:{3 4} b:{1 2}]
只声明的map,零值是nil,nil map不能添加key/values,即没有下面的make则后面不能操作。
声明、创建这块也可以直接写为m := map[string]Vertex
映射字面值(map literal):
var m = map[string]Vertex{ "a": Vertex{3, 4}, "b": Vertex{1, 2}, // 注意逗号 } // If the top-level type is just a type name, you can omit it from the elements of the literal. // 按我理解是表示 map 已经定义了值的类型,所以在里面的字面值不需要再定义 // 下面是简写,省去内部的值类型 var m = map[string]Vertex{ "a": {3, 4}, "b": {1, 2}, }
map的几个操作:
// 修改某个key的value m[k] = v // 获取某个key的value v = m[k] // 删除某个key delete(m, k) // 测试某个key是否在m中 // ok是boolean,存在则为true,否则是false v, ok = m[k]
Methods
Go没有类,但是可以给自定义类型(如结构体)定义方法(methods)。
method 和 function 类似,只不过多了一个特殊的接收者参数(receiver),位置在 func 关键字和 method name 之间。
// 如这里定义Abs这个方法,属于Vertex这个结构体,`(v Vertex)`就是function没有的多出的部分 func (v Vertex) AbsMethod() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } // 调用 v := Vertex{3, 4} fmt.Println(v.AbsMethod()) // 这个是function, 将Vertex结构体当普通参数 func AbsFunc(v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } // 调用 v := Vertex{3, 4} fmt.Println(AbsFunc(v))
如上所说,不光结构体可以声明方法,比如自定义类型:
type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) }
但是 receiver的类型必须定义在当前包里,不能给其它包里定义的类型声明method,比如内置类型。
上面使用的 receiver 是一个值(value receiver),receiver还可以是一个指针(pointer receiver),如:
// method func (v *Vertex) ScaleMethod(f float64) { v.X = v.X * f v.Y = v.Y * f } // function func ScaleFunc(v *Vertex, f float64) { v.X = v.X * f v.Y = v.Y * f }
因为Go里函数参数是传值,相当于一份拷贝,所以如果使用 func (v Vertex) Scale...
而不是 func (v *Vertex) Scale...
,则实际 v.X 和 v.Y 的值并没有被改变。
关于这块调用 Scale 时针对 pointer 和 value,需要注意一个坑:
var v Vertex // 针对上面的Scale ScaleFunc(v, 3) // compile error! ScaleFunc(&v, 3) // ok v.ScaleMethod(3) // ok, as (&v).ScaleMethod() p := &v p.ScaleMethod(3) // ok // 针对上上面的Abs AbsFunc(v) // ok AbsFunc(&v) // compile error! v.AbsMethod() // ok p := &v p.AbsMethod() // ok, as (*p).AbsMethod()
为了方便,Go的解释器对 method 作了一些自动化处理,如上例子,不论是 pointer receiver 还是 value receiver 的方法,都可以通过 pointer 或 value 来调用。
更倾向于选择使用 pointer receiver 的原因有两个:
- method 内部可以修改 receiver 的值
- 不是按值传递,所以更节省空间,比如需要传递的是一个很大的结构体
Interfaces
Go Tour上这块英文感觉有点绕,需要多读几遍。
An interface type is defined as a set of method signatures.
一个接口类型是一组 method 的定义的集合。
A value of interface type can hold any value that implements those methods.
接口类型是一个抽象类型,它的值可以是任何值,只需要这个值实现了接口的 methods
接口的好处就是将接口的定义和实现分离。
// 接口类型 type I interface { M() } // 具体类型 type T struct { S string } func (t *T) M() { fmt.Println(t.S) } // 具体类型 type F float64 func (f F) M() { fmt.Println(f) } func main() { var i I i = &T{"Hello"} fmt.Printf("(%v, %T)\n", i, i) i.M() i = F(0.11) fmt.Printf("(%v, %T)\n", i, i) i.M() } // 输出 // (&{Hello}, *main.T) // Hello // (0.11, main.F) // 0.11
一个接口需要挂载到一个底层具体类型上,调用接口的方法实际就是调用底层具体类型的同名方法.
如果实际类型是 nil (nil underlying value),则接口也是 nil
但如果接口类型是 nil (nil interface value),则无法调用它的 method,否则报错
空接口定义: var i interface{}
,接口i可以是任何值。
类型断言(type assertion)让接口值可以访问所挂载具体类型的值: t := i.(T)
,其中i是接口值,T是具体类型名:
var i interface{} = "hello" s := i.(string) fmt.Println(s) s, ok := i.(string) fmt.Println(s, ok) // 这个和之前的map test类似 f, ok := i.(float64) fmt.Println(f, ok) f = i.(float64) // panic fmt.Println(f)
type switch 结合了接口类型和switch语句,语法i.(type)
,和type assertion语法有点像。
// i is interface switch v := i.(type) { case int: fmt.Printf("Twice %v is %v\n", v, v*2) case string: fmt.Printf("%q is %v bytes long\n", v, len(v)) default: fmt.Printf("I don't know about type %T!\n", v) }
fmt包中定义了Stringer接口,方法String(),如目前用到最多的fmt.Println()就根据类型的String()方法输出内容:
// type Stringer interface { // String() string // } type Person struct { Name string Age int } func (p Person) String() string { return fmt.Sprintf("%v (%v years)", p.Name, p.Age) } func main() { a := Person{"Arthur Dent", 42} fmt.Println(a)
goroutine
轻量级线程(lightweight thread)
goroutines在同一个地址空间中运行,所以访问共享内存必须进行同步
使用关键字go
,go func(x, y)
中x, y是在当前goroutine中定义,但是func的执行是在一个新的goroutine:
func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(s) } } func main() { go say("world") say("hello") }
如果当前goroutine结束,则新起的goroutine也会结束。所以这个和执行时间、顺序有关系,上面的例子会出现偶尔world只输出4次的情况。如果去掉time.Sleep,则可能会出现world还没来得及输出就已经结束了。
channel
channel是一个有类型的管道(typed conduit),可以用来接收或发送数据,操作符<-
,channel 和 <-
的方向表示了数据流的方向:
ch <- v // 发送v到channel ch v := <-ch // 接收来自channel ch的数据,并赋值给v
和slice,map类似,channel 在使用前需要创建:
ch := make(chan int)
默认情况下,在另一端准备好之前,发送和接收都会阻塞。这使得 goroutine 可以在没有明确的锁或竞态变量的情况下进行同步。
package main import "fmt" func sum(s []int, c chan int) { sum := 0 for _, v := range s { sum += v } c <- sum // send sum to c } func main() { s := []int{7, 2, 8, -9, 4, 0} c := make(chan int) go sum(s[:len(s)/2], c) go sum(s[len(s)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
上面例子是goroutine和channel的配合。
创建channel时,第二个参数可以指定channel大小,使其为buffered channel:
package main import "fmt" func main() { ch := make(chan int, 2) ch <- 1 ch <- 2 fmt.Println(<-ch) fmt.Println(<-ch) }
如果是buffered channel,则如果buffer满了,则发送给channel会被block; 如果buffer空了,则从channel读取数据会被block。
上面例子如果在<-ch之前再写数据进ch,会导致报错: fatal error: all goroutines are asleep - deadlock!
在读取channel时,如果指定第二个参数,可以确认channel是否关闭。对channel进行for循环可以持续从channel读取数据,直到channel关闭。
ch := make(chan int) close(ch) // 关闭channel v, ok := <-ch // 检查channel是否关闭 for i := range ch { // 持续从channel读取数据 ... }
select
语句用于从多个channel中选出一个可用的channel来执行,都没有则block,如果有多个则随机选一个; 如果有default
case,则不会block,没有任何case可执行时则用default case
select { case <-c1: ... case <-c2: ... default: ... }