变量的初始化
变量声明可以包含初始值,每个变量对应一个。
如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" var i, j = 1 , 2 func main () { var c, python, java = true , false , "no!" fmt.Println(i, j, c, python, java) }
短变量声明
在函数中,简洁赋值语句 :=
可在类型明确的地方代替 var
声明。
函数外的每个语句都必须以关键字开始(var
, func
等等),因此 :=
结构不能在函数外使用。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { var i, j int = 1 , 2 k := 3 c, python, java := true , false , "no!" fmt.Println(i, j, k, c, python, java) }
基本类型
Go 的基本类型有
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 bool string int int8 int16 int32 int64 uint uint8 uint16 uint32 uint64 uintptr byte // uint8 的别名 rune // int32 的别名 // 表示一个 Unicode 码点 float32 float64 complex64 complex128
本例展示了几种类型的变量。 同导入语句一样,变量声明也可以“分组”成一个语法块。
int
, uint
和 uintptr
在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int
类型,除非你有特殊的理由使用固定大小或无符号的整数类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "math/cmplx" ) var ( ToBe bool = false MaxInt uint64 = 1 <<64 - 1 z complex128 = cmplx.Sqrt(-5 + 12i ) ) func main () { fmt.Printf("Type: %T Value: %v\n" , ToBe, ToBe) fmt.Printf("Type: %T Value: %v\n" , MaxInt, MaxInt) fmt.Printf("Type: %T Value: %v\n" , z, z) }
类型转换
表达式 T(v)
将值 v
转换为类型 T
。
一些关于数值的转换:
1 2 3 var i int = 42 var f float64 = float64(i) var u uint = uint(f)
或者,更加简单的形式:
1 2 3 i := 42 f := float64(i) u := uint(f)
与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。移除下面源代码中 float64
或 uint
就将报错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport ( "fmt" "math" ) func main () { var x, y int = 3 , 4 var f float64 = math.Sqrt(float64 (x*x + y*y)) var z uint = uint (f) fmt.Println(x, y, z) }
defer
defer 语句会将函数推迟到外层函数返回之后执行。
推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
1 2 3 4 5 6 7 8 9 10 package mainimport "fmt" func main () { defer fmt.Println("world" ) fmt.Println("hello" ) }
defer 栈
推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。
更多关于 defer 语句的信息,请阅读此博文 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package mainimport "fmt" func main () { fmt.Println("counting" ) for i := 0 ; i < 10 ; i++ { defer fmt.Println(i) } fmt.Println("done" ) }
指针
Go 拥有指针。指针保存了值的内存地址。
类型 *T
是指向 T
类型值的指针。其零值为 nil
。
&
操作符会生成一个指向其操作数的指针。
*
操作符表示指针指向的底层值。
1 2 fmt.Println(*p) // 通过指针 p 读取 i *p = 21 // 通过指针 p 设置 i
这也就是通常所说的“间接引用”或“重定向”。
与 C 不同,Go 没有指针运算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" func main () { i, j := 42 , 2701 p := &i fmt.Println(*p) *p = 21 fmt.Println(i) p = &j *p = *p / 37 fmt.Println(j) }
结构体指针
结构体字段可以通过结构体指针来访问。
如果我们有一个指向结构体的指针 p
,那么可以通过 (*p).X
来访问其字段 X
。不过这么写太啰嗦了,所以语言也允许我们使用隐式间接引用,直接写 p.X
就可以。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package mainimport "fmt" type Vertex struct { X int Y int } func main () { v := Vertex{1 , 2 } p := &v p.X = 1e9 fmt.Println(v.X) }
结构体文法
结构体文法通过直接列出字段的值来新分配一个结构体。
使用 Name:
语法可以仅列出部分字段。(字段名的顺序无关。)
特殊的前缀 &
返回一个指向结构体的指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" type Vertex struct { X, Y int } var ( v1 = Vertex{1 , 2 } v2 = Vertex{X: 1 } v3 = Vertex{} p = &Vertex{1 , 2 } ) func main () { fmt.Println(v1, p, v2, v3) }
结果
1 {1 2} &{1 2} {1 0} {0 0}
数组
类型 [n]T
表示拥有 n
个 T
类型的值的数组。
表达式
会将变量 a
声明为拥有 10 个整数的数组。
数组的长度是其类型的一部分,因此数组不能改变大小。这看起来是个限制,不过没关系,Go 提供了更加便利的方式来使用数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package mainimport "fmt" 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) }
结果
1 2 3 Hello World [Hello World] [2 3 5 7 11 13]
切片
每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。
类型 []T
表示一个元素类型为 T
的切片。
切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:
它会选择一个半开区间,包括第一个元素,但排除最后一个元素。
以下表达式创建了一个切片,它包含 a
中下标从 1 到 3 的元素:
1 2 3 4 5 6 7 8 9 10 11 package mainimport "fmt" func main () { primes := [6 ]int {2 , 3 , 5 , 7 , 11 , 13 } var s []int = primes[1 :4 ] fmt.Println(s) }
切片就像数组的引用
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" 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) }
结果
1 2 3 4 [John Paul George Ringo] [John Paul] [Paul George] [John XXX] [XXX George] [John XXX George Ringo]
切片的默认行为
在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界的默认值为 0
,上界则是该切片的长度。
对于数组
来说,以下切片是等价的:
1 2 3 4 a[0:10] a[:10] a[0:] a[:]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package main import "fmt" func main() { s := []int{2, 3, 5, 7, 11, 13} s = s[1:4] fmt.Println(s) s = s[:2] fmt.Println(s) s = s[1:] fmt.Println(s) }
结果
切片的长度与容量
切片拥有 长度 和 容量 。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s
的长度和容量可通过表达式 len(s)
和 cap(s)
来获取。
你可以通过重新切片来扩展一个切片,给它提供足够的容量。试着修改示例程序中的切片操作,向外扩展它的容量,看看会发生什么。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func main () { s := []int {2 , 3 , 5 , 7 , 11 , 13 } printSlice(s) s = s[:0 ] printSlice(s) s = s[:4 ] printSlice(s) s = s[2 :] printSlice(s) } func printSlice (s []int ) { fmt.Printf("len=%d cap=%d %v\n" , len (s), cap (s), s) }
1 2 3 4 len=6 cap=6 [2 3 5 7 11 13] len=0 cap=6 [] len=4 cap=6 [2 3 5 7] len=2 cap=4 [5 7]
nil 切片
切片的零值是 nil
。
nil 切片的长度和容量为 0 且没
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" func main () { var s []int fmt.Println(s, len (s), cap (s)) if s == nil { fmt.Println("nil!" ) } }
用 make 创建切片
切片可以用内建函数 make
来创建,这也是你创建动态数组的方式。
make
函数会分配一个元素为零值的数组并返回一个引用了它的切片:
1 a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make
传入第三个参数:
1 2 3 4 b := make([]int, 0, 5) // len(b)=0, cap(b)=5 b = b[:cap(b)] // len(b)=5, cap(b)=5 b = b[1:] // len(b)=4, cap(b)=
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" 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) }
1 2 3 4 a len=5 cap=5 [0 0 0 0 0] b len=0 cap=5 [] c len=2 cap=5 [0 0] d len=3 cap=3 [
向切片追加元素
为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append
函数。内建函数的文档 对此函数有详细的介绍。
1 func append(s []T, vs ...T) []T
append
的第一个参数 s
是一个元素类型为 T
的切片,其余类型为 T
的值将会追加到该切片的末尾。
append
的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s
的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 package mainimport "fmt" func main () { var s []int printSlice(s) s = append (s, 0 ) printSlice(s) s = append (s, 1 ) printSlice(s) s = append (s, 2 , 3 , 4 ) printSlice(s) } func printSlice (s []int ) { fmt.Printf("len=%d cap=%d %v\n" , len (s), cap (s), s) }
1 2 3 4 len=0 cap=0 [] len=1 cap=1 [0] len=2 cap=2 [0 1] len=5 cap=6 [0 1 2 3 4]
Range
for
循环的 range
形式可遍历切片或映射。
当使用 for
循环遍历切片时,每次迭代都会返回两个值。第一个值为当前元素的下标,第二个值为该下标所对应元素的一份副本。
1 2 3 4 5 6 7 8 9 10 11 12 package mainimport "fmt" var pow = []int {1 , 2 , 4 , 8 , 16 , 32 , 64 , 128 }func main () { for i, v := range pow { fmt.Printf("2**%d = %d\n" , i, v) } }
1 2 3 4 5 6 7 8 9 2**0 = 1 2**1 = 2 2**2 = 4 2**3 = 8 2**4 = 16 2**5 = 32 2**6 = 64 2**7 = 128
映射
映射将键映射到值。
映射的零值为 nil
。nil
映射既没有键,也不能添加键。
make
函数会返回给定类型的映射,并将其初始化备用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" type Vertex struct { Lat, Long float64 } var m map [string ]Vertexfunc main () { m = make (map [string ]Vertex) m["Bell Labs" ] = Vertex{ 40.68433 , -74.39967 , } fmt.Println(m["Bell Labs" ]) }
映射的文法
映射的文法与结构体相似,不过必须有键名。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "fmt" type Vertex struct { Lat, Long float64 } var m = map [string ]Vertex{ "Bell Labs" : Vertex{ 40.68433 , -74.39967 , }, "Google" : Vertex{ 37.42202 , -122.08408 , }, } func main () { fmt.Println(m) }
1 map[Bell Labs:{40.68433 -74.39967} Google:{37.42202 -122.08408}]
映射的文法(续)
若顶级类型只是一个类型名,你可以在文法的元素中省略它。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package mainimport "fmt" type Vertex struct { Lat, Long float64 } var m = map [string ]Vertex{ "Bell Labs" : {40.68433 , -74.39967 }, "Google" : {37.42202 , -122.08408 }, } func main () { fmt.Println(m) }
修改映射
在映射 m
中插入或修改元素:
获取元素:
删除元素:
通过双赋值检测某个键是否存在:
若 key
在 m
中,ok
为 true
;否则,ok
为 false
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import "fmt" func main() { m := make(map[string]int) m["Answer"] = 42 fmt.Println("The value:", m["Answer"]) m["Answer"] = 48 fmt.Println("The value:", m["Answer"]) delete(m, "Answer") fmt.Println("The value:", m["Answer"]) v, ok := m["Answer"] fmt.Println("The value:", v, "Present?", ok) }
1 2 3 4 The value: 42 The value: 48 The value: 0 The value: 0 Present? false
函数值
函数也是值。它们可以像其它值一样传递。
函数值可以用作函数的参数或返回值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport ( "fmt" "math" ) func compute (fn func (float64 , float64 ) float64 ) float64 { return fn(3 , 4 ) } func main () { hypot := func (x, y float64 ) float64 { return math.Sqrt(x*x + y*y) } fmt.Println(hypot(5 , 12 )) fmt.Println(compute(hypot)) fmt.Println(compute(math.Pow)) }
函数的闭包
Go 函数可以是一个闭包。闭包是一个函数值,它引用了其函数体之外的变量。该函数可以访问并赋予其引用的变量的值,换句话说,该函数被这些变量“绑定”在一起。
例如,函数 adder
返回一个闭包。每个闭包都被绑定在其各自的 sum
变量上
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package main import "fmt" 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), ) } }
1 2 3 4 5 6 7 8 9 10 0 0 1 -2 3 -6 6 -12 10 -20 15 -30 21 -42 28 -56 36 -72 45 -90
方法
Go 没有类。不过你可以为结构体类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
方法接收者在它自己的参数列表内,位于 func
关键字和方法名之间。
在此例中,Abs
方法拥有一个名为 v
,类型为 Vertex
的接收者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (b Vertex) Abs() float64 { return math.Sqrt(b.X*b.X + b.Y*b.Y) } func main () { v := Vertex{3 , 4 } fmt.Println(v.Abs()) }
方法即函数
记住:方法只是个带接收者参数的函数。
现在这个 Abs
的写法就是个正常的函数,功能并没有什么变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport ( "fmt" "math" ) type Vertex struct { X, Y float64 } func Abs (v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main () { v := Vertex{3 , 4 } fmt.Println(Abs(v)) }
方法(续)
你也可以为非结构体类型声明方法。
在此例中,我们看到了一个带 Abs
方法的数值类型 MyFloat
。
你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int
之类的内建类型)的接收者声明方法。
(译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport ( "fmt" "math" ) type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64 (-f) } return float64 (f) } func main () { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }
指针接收者
你可以为指针接收者声明方法。
这意味着对于某类型 T
,接收者的类型可以用 *T
的文法。(此外,T
不能是像 *int
这样的指针。)
例如,这里为 *Vertex
定义了 Scale
方法。
指针接收者的方法可以修改接收者指向的值(就像 Scale
在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
试着移除第 16 行 Scale
函数声明中的 *
,观察此程序的行为如何变化。
若使用值接收者,那么 Scale
方法会对原始 Vertex
值的副本进行操作。(对于函数的其它参数也是如此。)``Scale方法必须用指针接受者来更改
main函数中声明的
Vertex` 的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "math" ) 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 } func main () { v := Vertex{3 , 4 } v.Scale(10 ) fmt.Println(v.Abs()) }
指针与函数
现在我们要把 Abs
和 Scale
方法重写为函数。
同样,我们先试着移除掉第 16 的 *
。你能看出为什么程序的行为改变了吗?要怎样做才能让该示例顺利通过编译?
(若你不确定,继续往下看。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "math" ) type Vertex struct { X, Y float64 } func Abs (v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func Scale (v *Vertex, f float64 ) { v.X = v.X * f v.Y = v.Y * f } func main () { v := Vertex{3 , 4 } Scale(&v, 10 ) fmt.Println(Abs(v)) }
方法与指针重定向
比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:
1 2 3 var v VertexScaleFunc(v, 5 ) ScaleFunc(&v, 5 )
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4 var v Vertexv.Scale(5 ) p := &v p.Scale(10 )
对于语句 v.Scale(5)
,即便 v
是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale
方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5)
解释为 (&v).Scale(5)
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport "fmt" type Vertex struct { X, Y float64 } func (v *Vertex) Scale(f float64 ) { v.X = v.X * f v.Y = v.Y * f } func ScaleFunc (v *Vertex, f float64 ) { v.X = v.X * f v.Y = v.Y * f } func main () { v := Vertex{3 , 4 } v.Scale(2 ) ScaleFunc(&v, 10 ) p := &Vertex{4 , 3 } p.Scale(3 ) ScaleFunc(p, 8 ) fmt.Println(v, p) }
方法与指针重定向(续)
同样的事情也发生在相反的方向。
接受一个值作为参数的函数必须接受一个指定类型的值:
1 2 3 var v Vertex fmt.Println(AbsFunc(v)) // OK fmt.Println(AbsFunc(&v)) // 编译错误!
而以值为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4 var v Vertex fmt.Println(v.Abs()) // OK p := &v fmt.Println(p.Abs()) // OK
这种情况下,方法调用 p.Abs()
会被解释为 (*p).Abs()
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package mainimport ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func AbsFunc (v Vertex) float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main () { v := Vertex{3 , 4 } fmt.Println(v.Abs()) fmt.Println(AbsFunc(v)) p := &Vertex{4 , 3 } fmt.Println(p.Abs()) fmt.Println(AbsFunc(*p)) }
选择值或指针作为接收者
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
在本例中,Scale
和 Abs
接收者的类型为 *Vertex
,即便 Abs
并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。(我们会在接下来几页中明白为什么。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 package mainimport ( "fmt" "math" ) type Vertex struct { X, Y float64 } func (v *Vertex) Scale(f float64 ) { v.X = v.X * f v.Y = v.Y * f } func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) } func main () { v := &Vertex{3 , 4 } fmt.Printf("Before scaling: %+v, Abs: %v\n" , v, v.Abs()) v.Scale(5 ) fmt.Printf("After scaling: %+v, Abs: %v\n" , v, v.Abs()) }
1 2 Before scaling: &{X:3 Y:4}, Abs: 5 After scaling: &{X:15 Y:20}, Abs: 25
接口
接口类型 是由一组方法签名定义的集合。
接口类型的变量可以保存任何实现了这些方法的值。
注意: 示例代码的 22 行存在一个错误。由于 Abs
方法只为 *Vertex
(指针类型)定义,因此 Vertex
(值类型)并未实现 Abser
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 package mainimport ( "fmt" "math" ) type Abser interface { Abs() float64 } func main () { var a Abser f := MyFloat(-math.Sqrt2) v := Vertex{3 , 4 } a = f a = &v a = v fmt.Println(a.Abs()) } type MyFloat float64 func (f MyFloat) Abs() float64 { if f < 0 { return float64 (-f) } return float64 (f) } type Vertex struct { X, Y float64 } func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }
接口与隐式实现
类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport "fmt" type I interface { M() } type T struct { S string } func (t T) M() { fmt.Println(t.S) } func main () { var i I = T{"hello" } i.M() }
接口值
接口也是值。它们可以像其它值一样传递。
接口值可以用作函数的参数或返回值。
在内部,接口值可以看做包含值和具体类型的元组:
接口值保存了一个具体底层类型的具体值。
接口值调用方法时会执行其底层类型的同名方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package mainimport ( "fmt" "math" ) 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" } describe(i) i.M() i = F(math.Pi) describe(i) i.M() } func describe (i I) { fmt.Printf("(%v, %T)\n" , i, i) }
1 2 3 4 5 (&{Hello}, *main.T) Hello (3.141592653589793 , main.F) 3.141592653589793
底层值为 nil 的接口值
即便接口内的具体值为 nil,方法仍然会被 nil 接收者调用。
在一些语言中,这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M
方法)。
注意: 保存了 nil 具体值的接口其自身并不为 nil。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 package mainimport "fmt" type I interface { M() } type T struct { S string } func (t *T) M() { if t == nil { fmt.Println("<nil>" ) return } fmt.Println(t.S) } func main () { var i I var t *T i = t describe(i) i.M() i = &T{"hello" } describe(i) i.M() } func describe (i I) { fmt.Printf("(%v, %T)\n" , i, i) }
1 2 3 4 5 (<nil>, *main.T) <nil> (&{hello}, *main.T) hello
nil 接口值
nil 接口值既不保存值也不保存具体类型。
为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" type I interface { M() } func main () { var i I describe(i) i.M() } func describe (i I) { fmt.Printf("(%v, %T)\n" , i, i) }
1 2 3 4 5 6 7 8 (<nil>, <nil>) panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x492e3f] goroutine 1 [running]: main.main() /tmp/sandbox891365133/prog.go:12 +0x8f
空接口
指定了零个方法的接口值被称为 空接口:
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
空接口被用来处理未知类型的值。例如,fmt.Print
可接受类型为 interface{}
的任意数量的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var i interface {} describe(i) i = 42 describe(i) i = "hello" describe(i) } func describe (i interface {}) { fmt.Printf("(%v, %T)\n" , i, i) }
1 2 3 (<nil>, <nil>) (42, int) (hello, string)
类型断言
类型断言 提供了访问接口值底层具体值的方式。
该语句断言接口值 i
保存了具体类型 T
,并将其底层类型为 T
的值赋予变量 t
。
若 i
并未保存 T
类型的值,该语句就会触发一个恐慌。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
若 i
保存了一个 T
,那么 t
将会是其底层值,而 ok
为 true
。
否则,ok
将为 false
而 t
将为 T
类型的零值,程序并不会产生恐慌。
请注意这种语法和读取一个映射时的相同之处。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 package mainimport "fmt" func main () { var i interface {} = "hello" s := i.(string ) fmt.Println(s) s, ok := i.(string ) fmt.Println(s, ok) f, ok := i.(float64 ) fmt.Println(f, ok) f = i.(float64 ) fmt.Println(f) }
1 2 3 4 hello hello true 0 false panic: interface conversion: interface {} is string, not float64
类型选择
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。
1 2 3 4 5 6 7 8 switch v := i.(type) { case T: // v 的类型为 T case S: // v 的类型为 S default: // 没有匹配,v 与 i 的类型相同 }
类型选择中的声明与类型断言 i.(T)
的语法相同,只是具体类型 T
被替换成了关键字 type
。
此选择语句判断接口值 i
保存的值类型是 T
还是 S
。在 T
或 S
的情况下,变量 v
会分别按 T
或 S
类型保存 i
拥有的值。在默认(即没有匹配)的情况下,变量 v
与 i
的接口类型和值相同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 package mainimport "fmt" func do (i 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) } } func main () { do(21 ) do("hello" ) do(true ) }
1 2 3 Twice 21 is 42 "hello" is 5 bytes long I don't know about type bool!
Stringer
fmt
包中定义的 Stringer
是最普遍的接口之一。
1 2 3 type Stringer interface { String() string }
Stringer
是一个可以用字符串描述自己的类型。fmt
包(还有很多包)都通过此接口来打印值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport "fmt" 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 } z := Person{"Zaphod Beeblebrox" , 9001 } fmt.Println(a, z) }
1 Arthur Dent (42 years) Zaphod Beeblebrox (9001 years)
错误
Go 程序使用 error
值来表示错误状态。
与 fmt.Stringer
类似,error
类型是一个内建接口:
1 2 3 type error interface { Error() string }
(与 fmt.Stringer
类似,fmt
包在打印值时也会满足 error
。)
通常函数会返回一个 error
值,调用的它的代码应当判断这个错误是否等于 nil
来进行错误处理。
1 2 3 4 5 6 i, err := strconv.Atoi("42") if err != nil { fmt.Printf("couldn't convert number: %v\n", err) return } fmt.Println("Converted integer:", i)
error
为 nil 时表示成功;非 nil 的 error
表示失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 package mainimport ( "fmt" "time" ) type MyError struct { When time.Time What string } func (e *MyError) Error() string { return fmt.Sprintf("at %v, %s" , e.When, e.What) } func run () error { return &MyError{ time.Now(), "it didn't work" , } } func main () { if err := run(); err != nil { fmt.Println(err) } }
Reader
io
包指定了 io.Reader
接口,它表示从数据流的末尾进行读取。
Go 标准库包含了该接口的许多实现 ,包括文件、网络连接、压缩和加密等等。
io.Reader
接口有一个 Read
方法:
1 func (T) Read(b []byte) (n int, err error)
Read
用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF
错误。
示例代码创建了一个 strings.Reader
并以每次 8 字节的速度读取它的输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package mainimport ( "fmt" "io" "strings" ) func main () { r := strings.NewReader("Hello, Reader!" ) b := make ([]byte , 8 ) for { n, err := r.Read(b) fmt.Printf("n = %v err = %v b = %v\n" , n, err, b) fmt.Printf("b[:n] = %q\n" , b[:n]) if err == io.EOF { break } } }
1 2 3 4 5 6 7 n = 8 err = <nil> b = [72 101 108 108 111 44 32 82] b[:n] = "Hello, R" n = 6 err = <nil> b = [101 97 100 101 114 33 32 82] b[:n] = "eader!" n = 0 err = EOF b = [101 97 100 101 114 33 32 82] b[:n] = ""