程序结构
包
按照约定,包名与包路径的最后一个元素相同。例如 math/rand
包中的源码均以 package rand
开始。
可以使用括号来对 import
语句进行分组,对 import
语句进行分组是一种好的风格。
在 Go 中,没有 public
、protected
、private
关键字。若(变量、函数……)的 名称是以大写字母开头,则表示它们是 exported 的,即可以被外部包中的代码所访问。
函数
函数可以接收零个或多个参数。Go 的 类型是位于名称之后 的,至于为什么这么设计,可以看 Go’s Declaration Syntax。
func add(x int, y int) int {
return x + y
}
当函数中有两个或者更多连续相同类型的形式参数,则可以省略除最后一个参数之外的所有参数的类型声明。
func add(x, y int) int {
return x + y
}
函数可以返回任意数量的结果。
函数的返回值可以被命名,它们会被视为在函数顶部定义的变量。
变量
var
关键字可以声明一个变量列表,变量的类型也是位于名称之后的。变量声明语句可以位于包级别和函数级别。
变量的声明可以包含初始化值,每个变量对应一个值。变量的声明也可以使用括号来进行分组。
变量的声明中若包含初始化值,则可以省略变量的类型定义。该变量的类型会从初始化值中推导得出。
在函数内部,:=
赋值语句可以替换隐式类型声明的变量声明语句。在函数外部,每个语句都必须以 Go 关键字开始(var
、func
……),所以不能使用 :=
语法。
Go 的基础类型有:
bool
string
int int8 int16 int32 int64
uint uint8 uint16 uint32 uint64 uintptr
byte // alias for uint8
rune // alias for int32
// represents a Unicode code point
float32 float64
complex64 complex128
没有被明确初始化的变量,会被赋予。数值类型变量的零值是 0,布尔类型变量的零值是 false,字符串类型变量的零值是空字符串。
表达式 T(v)
用于将值 v 转换成类型 T。
在变量初始化的类型推导中,当右侧是没有类型的数值常量时,声明的变量可能会是 int
、float64
或 complex128
,这取决于数值常量的精度。
i := 42 // int
f := 3.142 // float64
g := 0.867 + 0.5i // complex128
const
关键字可以声明常量,常量可以是字符、字符串、布尔值、数值。常量不可以使用 :=
语法赋值。
控制流程
for
Go 中只有 for
语句一种循环结构。Go 的 for
语句由逗号分隔的三个部分组成:初始化语句、条件语句、后置语句,它们不需要由括号包裹,并且都是可以忽略的。
func main() {
sum := 0
for i := 0; i < 10; i++ {
sum += i
}
fmt.Println(sum)
}
func main() {
sum := 1
for ; sum < 1000; {
sum += sum
}
fmt.Println(sum)
}
在 for
语句中,当忽略初始化语句和后置语句时,也可以忽略 ;
符号,这便是 Go 中的 where
语句。
func main() {
sum := 1
for sum < 1000 {
sum += sum
}
fmt.Println(sum)
}
在 for
语句中,当连条件语句也被忽略时,则表示无限循环。
if-else
if
语句和 for
语句类似,判断条件也不需要由括号包裹。
if
语句和 for
语句类似,也可以在判断条件之前,执行一个只在当前作用域内有效的变量初始化语句。
在 if
语句中初始化的变量,在 else
语句中也是有效的。
switch
**switch**
语句只会执行匹配成功的 **case**
语句,而不是其后的所有 **case**
语句。实际上,Go 会在每个 case
语句末尾自动添加 break
语句。
switch
语句和 case
语句的条件语句不需要是常量,它们所涉及的值也不需要是整数。
func main() {
fmt.Print("Go runs on ")
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
}
**switch**
语句由上而下执行,在匹配成功时便会结束。在下例中,当 i==0
时,f()
是不会被调用的。
switch i {
case 0:
case f():
}
不带条件语句的 switch
语句等价于 switch true
,这种写法是另一种简洁的 if-then-else
语句。
func main() {
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.")
}
}
defer
defer
语句可以将待执行函数,推迟到当前函数返回之后再执行,但待执行函数的参数是会被立即执行的。
func main() {
defer fmt.Println("world")
fmt.Println("hello")
}
defer
语句的待执行函数会被压入到一个栈中,在当前函数返回之后,会按照 Last-In-First-Out 的顺序被执行。
func main() {
fmt.Println("counting")
for i := 0; i < 10; i++ {
defer fmt.Println(i)
}
fmt.Println("done")
}
数据结构
pointer
Go 中也是有指针的。指针保存的是值的内存地址。
类型 *T
表示一个指向类型 T
的指针,指针的零值是 nil
。
&
运算符用于生成一个指向其操作数的指针。*
运算符用于获取指针指向的值。
func main() {
i, j := 42, 2701
p := &i // point to i
fmt.Println(*p) // read i through the pointer
*p = 21 // set i through the pointer
fmt.Println(i) // see the new value of i
p = &j // point to j
*p = *p / 37 // divide j through the pointer
fmt.Println(j) // see the new value of j
}
与 C 语言不同,Go 中是没有指针运算的。
structs
结构(struct)是一组字段的集合,使用 .
符号来访问结构的字段。
type Vertex struct {
X int
Y int
}
func main() {
v := Vertex{1, 2}
v.X = 4
fmt.Println(v.X)
}
可以通过指向结构的指针来访问结构的字段,Go 中 (*p).X
的简写方式为 p.X
,对应了 C 语言中的 p->X
。
结构字面量表示通过列出字段值的方式,来分配的一个新结构。可以使用 Name:
的语法来仅列出字段的子集。
var (
v1 = Vertex{1, 2} // has type Vertex
v2 = Vertex{X: 1} // Y:0 is implicit
v3 = Vertex{} // X:0 and Y:0
p = &Vertex{1, 2} // has type *Vertex
)
array
类型 [n]T
表示一个由 n 个 T 类型的值组成的数组(Array)。数组的长度是其类型的一部分,数组的长度不可以被调整。
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
slice
类型 []T
表示一个由 T 类型的值组成的切片(slice)。切片的长度可以被动态调整。
切片是通过指定一对上下限的数组下标,并以冒号分隔而组成的。切片会包含上限对应的元素,但不会包含下限对应的元素。
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
切片不会存储任何数据,它只是展示了底层数组的一部分数据。修改切片的元素,也会修改底层数组的元素,其它共享底层数组的切片也会看到数据元素的变更。
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
切面字面量类似于数组字面量,只是不需要指定长度。
// 数组字面量
[3]bool{true, true, false}
// 切片字面量,创建了与上例数组字面量相同的数据,然后创建了引用该数组的切片
[]bool{true, true, false}
创建切片的时候,可以省略上下限的数组下标,并以默认值替代。下限的数组下标默认值为 0,上限的数组下标默认值为数组长度。
var a [10]int
// 以下创建切片语句,都是等价的
a[0:10]
a[:10]
a[0:]
a[:]
切片的长度表示切片包含的元素个数,切片的容量表示切片引用的底层数组的元素个数,从切片的第一个元素开始统计。切片的长度可以使用 len(s)
表达式来获取,切片的容量可以使用 cap(s)
表达式获取。可以通过重新定义切片的上下限来扩展切片的长度,但需要保证切片有足够的容量。
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// Slice the slice to give it zero length.
s = s[:0]
printSlice(s)
// Extend its length.
s = s[:4]
printSlice(s)
// Drop its first two values.
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
切片的零值是 nil,零值切片的长度和容量都是 0,并且没有引用的底层数组。
可以使用内置的 **make**
函数来创建切片。make
函数会创建一个元素均为零值的数组,然后返回引用该数组的切片。这便是 Go 中创建动态数组的方式。
func main() {
a := make([]int, 5)
printSlice("a", a)
b := make([]int, 0, 5)
printSlice("b", b)
c := b[:2]
printSlice("c", c)
d := c[2:5]
printSlice("d", d)
}
func printSlice(s string, x []int) {
fmt.Printf("%s len=%d cap=%d %v\n",
s, len(x), cap(x), x)
}
切片可以包含任何类型,甚至是其它切片。
可以使用内置的 **append**
函数来给切片追加元素。当被追加的切片容量不够时,append
函数则会创建一个更大的底层数组,并且返回的切片也会引用这个新创建的数组。更多关于切片的细节,可以看 Go Slices: usage and internals。
练习
package main
import "golang.org/x/tour/pic"
func Pic(dx, dy int) [][]uint8 {
var result [][]uint8 = make([][]uint8, dy)
for y, _ := range result {
result[y] = make([]uint8, dx)
for x, _ := range result[y] {
result[y][x] = uint8((x + y) / 2)
}
}
return result
}
func main() {
pic.Show(Pic)
}
range
在 for
循环中,可以使用 range
来遍历切片和映射。当遍历切片的时候,每次迭代会返回两个值,第一个是当前元素的下标,第二个是该下标对应的元素的值副本。
func main() {
var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}
for i, v := range pow {
fmt.Printf("2**%d = %d\n", i, v)
}
}
可以通过赋值 _
来忽略 range
返回的元素下标或者元素值副本。如果只需要元素下标,则可以忽略第二个返回值。
for i, _ := range pow
for _, value := range pow
for i := range pow
map
映射(map)用于将键(key)映射到值(value)。
映射的零值是 nil,零值映射没有任何 key,也不能添加 key。
可以使用内置的 **make**
函数来创建映射。make
函数会创建一个指定类型的映射,并会为其初始化。
type Vertex struct {
Lat, Long float64
}
var m map[string]Vertex
func main() {
m = make(map[string]Vertex)
m["Bell Labs"] = Vertex{
40.68433, -74.39967,
}
fmt.Println(m["Bell Labs"])
}
映射字面量类似于结构体字面量,但必须要指定键。
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": Vertex{
40.68433, -74.39967,
},
"Google": Vertex{
37.42202, -122.08408,
},
}
如果映射的值是一个类型,那么可以在字面量的元素中省略。
type Vertex struct {
Lat, Long float64
}
var m = map[string]Vertex{
"Bell Labs": {40.68433, -74.39967},
"Google": {37.42202, -122.08408},
}
插入或更新映射中的元素:m[key] = elem
,获取映射中的元素:elem = m[key]
,删除映射中的元素:delete(m, key)
,判断映射中是否存在键:elem, ok = m[key]
,若键不存在,则 elem
是该类型的零值,并且 ok
值为 false
。
练习
package main
import (
"strings"
"golang.org/x/tour/wc"
)
func WordCount(s string) map[string]int {
result := make(map[string]int)
for _, word := range strings.Fields(s) {
if _, ok := result[word]; ok {
result[word]++
} else {
result[word] = 1
}
}
return result
}
func main() {
wc.Test(WordCount)
}
function
函数(function)也是值,可以像其它值一样被传递。函数值可以被作为函数的参数和返回值。
Go 中的函数可以是闭包。闭包是一个函数值,它引用了函数体之外的变量。该函数可以访问和赋值它引用的变量。从这个意义上来说,闭包函数是被绑定到变量上的。
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
练习:斐波那契闭包
package main
import "fmt"
// fibonacci is a function that returns
// a function that returns an int.
func fibonacci() func() int {
a, b := 0, 1
return func() int {
fib := a
a = b
b = fib + b
return fib
}
}
func main() {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Println(f())
}
}
方法
Go 中没有 class,但是类型(Type)可以有方法(Method)。方法是一种带有接收者(Receiver)参数的特殊函数。
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
除了可以给「结构体类型」定义方法,也可以给「非结构体类型」定义方法。但需要注意的是,只能给当前包范围内的类型定义方法。
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
可以给「指针类型」定义方法,接收者为「指针类型」的方法可以修改接收者指针所指向的值。接收者为「值类型」的方法修改的只是值的副本,Go 中对函数参数的操作也是如此。
type Vertex struct {
X, Y float64
}
func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func (v *Vertex) Scale(f float64) {
v.X = v.X * f
v.Y = v.Y * f
}
接收者为「指针类型」的方法,既可以被指针所调用,也可以被值所调用。Go 会将 v.Scale(5)
语句解释为 (&v).Scale()
。同样的,接收者为「值类型」的方法,也都可以被指针和值所调用,Go 会将 p.Scale(5)
解释为 (*p).Scale(5)
。
通常都会定义接收者为「指针类型」的方法,主要有两点原因:
Go 中不需要「显式声明实现一个接口」,也没有 implements
关键字,实现一个接口只需实现它的所有方法即可。这种隐式的实现接口方式,可以解耦「接口的定义」和「接口的实现」。
在底层实现中,可以把接口类型认为是一个「值 + 具体类型」的元祖 (value, type)
。接口值保存的就是接口具体类型的值,调用接口值的方法就是在接口具体类型上调用接口值的同名方法。
值为 nil 的接口
如果接口值为 nil,则调用接口方法时的接收者就是 nil。在一些语言中,这种情况可能会导致一个空指针异常,但在 Go 中,可以优雅地处理这种情况。
type I interface {
M()
}
type T struct {
S string
}
func (t *T) M() {
if t == nil {
fmt.Println("<nil>")
return
}
fmt.Println(t.S)
}
nil 接口
接口值为 nil 的接口本身并不为 nil,nil 接口本身不包含任何值和类型,并且在 nil 接口上调用方法会导致一个运行时异常。
空接口
不包含任何方法的接口被称为空接口。空接口可以保存任何类型的值,通常被用于处理未知的类型。
类型断言
类型断言(Type Assertion)提供了访问接口值底层的具体类型的值的方式。如下,类型断言语句会断言接口值 i
底层的具体类型是否为 T
,然后将 T
类型的具体值赋值给变量 t
。如果接口值 i
底层的具体类型不是 T
,则会抛出一个异常。
t := i.(T)
类型断言可以返回两个值,第二个返回值用于判断接口值底层是否保存了某个具体类型。如下,如果接口值 i
底层的具体类型是 T
,则 t
就是具体类型的值,ok
值为 true
。如果接口值 i
底层的具体类型不是 T
,则 t
是具体类型的零值,ok
值为 false
,并且程序不会抛出异常。
t, ok := i.(T)
类型选择
类型选择(Type Switch)是一种按序从多个类型断言中选择分支的结构。
switch v := i.(type) {
case T:
// here v has type T
case S:
// here v has type S
default:
// no match; here v has the same type as i
}
类型选择语句类似于标准的 switch
语句,不过 case
子句中匹配的是具体类型(而不是值),它们会被用于和接口值的具体类型所比较。
类型选择的声明部分类似于类型选择 v :=i.(T)
,不过具体类型 T
被替换成了关键字 type
。
常用接口
Stringer
fmt
包中定义了一个 Stringer
接口,用于将接口自身描述为一个字符串。fmt
包和其它很多包会使用 Stringer
接口来打印值。
type Stringer interface {
String() string
}
Error
error
是 Go 中内建的接口,用于表示程序的错误状态。类似于 fmt.Stringer
,fmt
包会使用 error
接口来打印错误值。
type error interface {
Error() string
}
函数通常都会返回一个 **error**
值,调用函数时则需要判断 **error**
值是否为 nil,**error**
值为 nil 表示函数调用成功,**error**
值不为 nil 表示函数调用失败。
Reader
io
包中定义了一个 Reader
接口,用于读取流中的数据。Go 标准库中有很多 Reader
接口的实现,包括文件读写、网络连接、压缩算法、加密算法等等。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
方法会填充给定的字节切片,然后返回填充的字节数量和错误值。错误值 io.EOF
表示已经读取到了流的末尾。
Image
image
包中定义了一个 Image
接口,用于表示一张图片。
type Image interface {
ColorModel() color.Model
Bounds() Rectangle
At(x, y int) color.Color
}
并发
goroutine
协程(goroutine)是由 Go 运行时管理的轻量级线程。
go f(x, y, z)
语句表示启动一个新的协程 f(x, y, z)
并执行。
f
、x
、y
、z
会在当前的协程中求值,f
函数会在新的协程中执行。
多个 Go 协程会在相同的地址空间中运行,因此在协程中访问共享的内存变量时必须保证同步。
channel
管道(channel)是带有类型的渠道,可以使用操作符 <-
在管道上发送和接收数据。
ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and
// assign value to v.
与映射和切片一样,管道必须先创建,然后才能使用:
ch := make(chan int)
操作符 <-
表示管道的方向(发送或者接收),如果没有指定方向,则表示管道是双向的。管道可以被限制为单向的(只能执行发送或者接收操作)。
var ch chan T // can be used to send and receive values of type T
var ch chan<- float64 // can only be used to send float64s
var ch <-chan int // can only be used to receive ints
在默认情况下,管道上的发送和接收操作会被阻塞,直到另一侧的操作已经准备就绪。这使得协程在没有显式锁或者条件变量的情况下可以实现同步操作。
管道是可以带缓冲的,把缓冲长度作为第二个参数赋值给 make
函数,就可以创建一个带缓冲的管道。
ch := make(chan int, 100)
当缓冲填满时,向管道发送数据会被阻塞。当缓冲为空时,从管道接收数据会被阻塞。
管道的发送者可以使用 close
函数来关闭管道,用于表示不再发送任何数据。管道的接收者可以通过接收表达式的第二个参数,来判断管道是否已经关闭。当管道已经关闭时,ok
值为 false
。
v, ok := <-ch
for i := range c
循环语句将会重复地从管道中获取数据,直至该管道被关闭为止。
只有管道的发送者才可以关闭管道,而不是管道的接收者。在一个已关闭的管道上发送数据时,将会导致程序异常。
管道不同于文件,通常不需要被关闭。仅当需要告诉接收者该管道已经不再发送数据的时候,才需要关闭管道,例如终止一个管道的 range
循环。
select
select
语句可以让一个协程等待多个通信操作。select
语句会一直阻塞,直到它的某个 case
语句可以运行为止,然后执行该 case
语句。如果有多个 case
语句准备就绪时,select
语句会随机选择一个来执行。
如果 select
语句中没有准备就绪的 case
语句时,则会执行 default
语句。
sync.Mutex
如果只是想要保证在同一个时刻,只有一个协程可以访问一个共享变量,从而避免产生冲突的话,则需要使用一种叫互斥锁(mutex)的数据结构。
Go 标准库中提供了 sync.Mutex
结构和它的 Lock
、Unlock
方法来实现互斥操作。可以通过调用 Lock
和 Unlock
方法来包裹一段代码块,来保证它的互斥执行。也可以通过 defer
语句来保证互斥锁一定会被解开。