1. 方法 1.1. 方法定义 Go 没有类。不过你可以为结构体类型定义方法。 方法就是一类带特殊的 接收者 参数的函数。 方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。 在此例中,changeName 方法拥有一个名为 p,类型为 Person 的接收者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package mainimport "fmt" type Person struct { name string age int } func (p Person) changeName() string { return "hello " + p.name; } func main () { person := Person{ name: "yuanmomo" , age: 20 , } fmt.Println(person.changeName()) } 输出结果: hello yuanmomo
1.2. 方法即函数 记住:方法只是个带接收者参数的函数。 现在这个 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 package mainimport "fmt" type Person struct { name string age int } func changeName (p Person) string { return "hello " + p.name; } func main () { person := Person{ name: "yuanmomo" , age: 20 , } fmt.Println(changeName(person)) } 输出结果: hello yuanmomo
1.3. 非结构体类型声明方法 你也可以为非结构体类型声明方法。 在此例中,我们看到了一个带 Abs 方法的数值类型 MyInt。你只能为在同一包内定义的类型的接收者声明方法,而不能为其它包内定义的类型(包括 int 之类的内建类型)的接收者声明方法。 (译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)
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 MyInt int func (i MyInt) abs() int { if i < 0 { return int (-i) } return int (i) } func main () { value := MyInt(-10 ) fmt.Println(value.abs()) } 输出结果: 10
1.4. 指针接受者(引用传递) 你可以为指针接收者声明方法。 这意味着对于某类型 T,接收者的类型可以用 *T 的文法。(此外,T 不能是像 *int 这样的指针。) 例如,这里为 *Vertex 定义了 Scale2=3 方法。 指针接收者的方法可以修改接收者指向的值(就像 Scale3 在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
若使用值接收者,那么 Scale2 方法会对原始 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 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) Scale1(f float64 ) { v.X = v.X * f v.Y = v.Y * f } func (v Vertex) Scale2(f float64 ) Vertex { v.X = v.X * f v.Y = v.Y * f return v } func (v *Vertex) Scale3(f float64 ) { v.X = v.X * f v.Y = v.Y * f } func Scale4 (v Vertex, f float64 ) Vertex { v.X = v.X * f v.Y = v.Y * f return v } func main () { v := Vertex{X:3 ,Y:4 } fmt.Printf("sqrt : %v \n" ,v.Abs()) v.Scale1(10 ) fmt.Printf("Scale1 after : %v \n" ,v.Abs()) v = v.Scale2(20 ) fmt.Printf("Scale2 after : %v \n" ,v.Abs()) v = Vertex{X:3 ,Y:4 } v.Scale3(30 ) fmt.Printf("Scale3 after : %v \n" ,v.Abs()) } 输出结果: sqrt : 5 Scale1 after : 5 Scale2 after : 100 Scale3 after : 150
1.5. 方法与指针重定向 指针参数的函数必须接受一个指针:
1 2 3 4 var v VertexScaleFunc(v, 5 ) ScaleFunc(&v, 5 )
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4 5 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 31 32 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) } 输出结果: {60 80 } &{96 72 }
同样的事情也发生在相反的方向。
接受一个值作为参数的函数必须接受一个指定类型的值:
1 2 3 4 var v Vertexfmt.Println(AbsFunc(v)) fmt.Println(AbsFunc(&v))
而以值为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4 5 var v Vertexfmt.Println(v.Abs()) p := &v fmt.Println(p.Abs())
这种情况下,方法调用 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 30 31 32 33 34 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)) } 输出结果: 5 5 5 5
1.6. 择值或指针作为接收者 使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。
在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。(我们会在接下来几页中明白为什么。)
2. 接口 接口类型 是由一组方法签名定义的集合。
接口类型的变量可以保存任何实现了这些方法的值。
注意: 示例代码的 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 43 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) }
2.1. 接口与隐式实现 类型通过实现一个接口的所有方法来实现该接口。既然无需专门显式声明,也就没有“implements”关键字。
隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。
因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 package mainimport "fmt" type Action interface { say(wordsToSay string ) string } type Person struct { name string } func (p Person) say(wordsToSay string ) string { return p.name +" says " + wordsToSay } func main () { var p1 Person = Person{name:"yuanmomo" } fmt.Println(p1.say("Hello World!!!" )) } 输出结果: yuanmomo says Hello World!!!
2.2. 接口值 (Java 多态) 接口也是值。它们可以像其它值一样传递。 接口值可以用作函数的参数或返回值。(Java 的多态) 在内部,接口值可以看做包含值和具体类型的元组:
接口值保存了一个具体底层类型的具体值。 接口值调用方法时会执行其底层类型的同名方法。
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 43 44 45 package mainimport "fmt" type Action interface { say(wordsToSay string ) string } type Person struct { name string } type Dog struct { name string typeName string } func (p Person) say(wordsToSay string ) string { return p.name + " says " + wordsToSay } func (d *Dog) say(wordsToSay string ) string { return d.typeName + " " + d.name + " cannot say " + wordsToSay + ", only can wang,wang,wang" } func main () { var action Action; action = &Dog{name: "milu" , typeName: "Dog" } describe(action) fmt.Println(action.say("Hello master" )) action = Person{name: "momo" } describe(action) fmt.Println(action.say("Hello milu" )) } func describe (action Action) { fmt.Printf("(%v,%T)\n" , action, action) } 输出结果: (&{milu Dog},*main.Dog) Dog milu cannot say Hello master, only can wang,wang,wang ({momo},main.Person) momo says Hello milu
2.3. 底层值为 nil 的接口值(Java 经典的 NPE) 即便接口内的具体值为 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 38 39 40 41 42 package mainimport "fmt" type Action interface { say(wordsToSay string ) string } type Dog struct { name string typeName string } func (d *Dog) say(wordsToSay string ) string { if d == nil { return "Param is nil !!!!" } return d.typeName + " " + d.name + " cannot say " + wordsToSay + ", only can wang,wang,wang" } func main () { var action Action var dog *Dog action = dog describe(action) fmt.Println(action.say("I am nil " )) action = &Dog{name: "milu" , typeName: "Dog" } describe(action) fmt.Println(action.say("Hello master" )) } func describe (action Action) { fmt.Printf("(%v,%T)\n" , action, action) } 输出结果: (<nil >,*main.Dog) Param is nil !!!! (&{milu Dog},*main.Dog) Dog milu cannot say Hello master, only can wang,wang,wang
2.4. nil 接口值 nil 接口值既不保存值也不保存具体类型。 为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
2.5. 空接口(Java Object) 指定了零个方法的接口值被称为 空接口:
空接口可保存任何类型的值。(因为每个类型都至少实现了零个方法。)
空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 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) } 输出结果: (<nil >, <nil >) (42 , int ) (hello, string )
3. 类型断言 (Java 类转型, Class cast) 类型断言 提供了访问接口值底层具体值的方式。
该语句断言接口值 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 20 21 22 23 24 25 26 27 28 29 30 package mainimport "fmt" func main () { var i interface {} = "Hello World" 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) } 输出结果: Hello World Hello World true 0 false panic : interface conversion: interface {} is string , not float64 goroutine 1 [running]: main.main() /Users/MoMo/go /src/momo-learn/main.go :18 +0x1f7
3.1. 类型选择 类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。
1 2 3 4 5 6 7 8 9 switch v := i.(type ) {case T: case S: default : }
类型选择中的声明与类型断言 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 22 23 24 25 26 package mainimport "fmt" func do (i interface {}) { switch v := i.(type ) { case string : fmt.Printf("%q is %v bytes long\n" ,v,len (v)) case int : fmt.Printf("Twice %v is %v\n" ,v,v*2 ) default : fmt.Printf("I don't know about type %T!\n" , v) } } func main () { do(21 ) do("hello" ) do(true ) } 输出结果: Twice 21 is 42 "hello" is 5 bytes longI don't know about type bool!
3.2. Stringer (Java toString) 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 19 20 21 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 () { p := Person{name: "yuanmomo" , age: 30 } fmt.Println(p.String()) } 输出结果: yuanmomo (30 years)
3.3. 练习:Stringer 通过让 IPAddr 类型实现 fmt.Stringer 来打印点号分隔的地址。
例如,IPAddr{1, 2, 3, 4} 应当打印为 “1.2.3.4”。
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 package mainimport ( "fmt" ) type IPAddr [4 ]byte func (ip IPAddr) String() string { var result string for _, seg := range ip { if len (result) > 0 { result += "." } result = fmt.Sprintf("%v%v" , result, seg) } return result } func main () { hosts := map [string ]IPAddr{ "loopback" : {127 , 0 , 0 , 1 }, "googleDNS" : {8 , 8 , 8 , 8 }, } for name, ip := range hosts { fmt.Printf("%v: %v\n" , name, ip) } } 输出结果: loopback: 127.0 .0 .1 googleDNS: 8.8 .8 .8
4. 错误 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 7 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 表示失败。
4.1. 练习:错误 从之前的练习中复制 Sqrt 函数,修改它使其返回 error 值。
Sqrt 接受到一个负数时,应当返回一个非 nil 的错误值。复数同样也不被支持。
创建一个新的类型
1 type ErrNegativeSqrt float64
并为其实现
1 func (e ErrNegativeSqrt) Error() string
方法使其拥有 error 值,通过 ErrNegativeSqrt(-2).Error() 调用该方法应返回 “cannot Sqrt negative number: -2”。
注意: 在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。这是为什么呢?
修改 Sqrt 函数,使其接受一个负数时,返回 ErrNegativeSqrt 值。
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 ErrNegativeSqrt float64 func (e ErrNegativeSqrt) Error(msg string ) string { return msg; } func Sqrt (x float64 ) (float64 , string ) { if x < 0 { return 0 ,ErrNegativeSqrt(x).Error(fmt.Sprintf("cannot Sqrt negative number: %v" ,x)) } return math.Sqrt(x),"" } func main () { fmt.Println(Sqrt(2 )) fmt.Println(Sqrt(-2 )) } 输出结果: 1.4142135623730951 0 cannot Sqrt negative number: -2
5. 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 23 24 25 26 27 28 29 30 31 32 package mainimport ( "fmt" "io" "strings" ) func main () { input := strings.NewReader("Hello World!!!" ) b := make ([]byte , 8 ) for { n, err := input.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 } } } 输出结果: n = 8 err = <nil > b= [72 101 108 108 111 32 87 111 ] b[:n] = "Hello Wo" n = 6 err = <nil > b= [114 108 100 33 33 33 87 111 ] b[:n] = "rld!!!" n = 0 err = EOF b= [114 108 100 33 33 33 87 111 ] b[:n] = ""
5.1. 练习:Reader 实现一个 Reader 类型,它产生一个 ASCII 字符 ‘A’ 的无限流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package mainimport "golang.org/x/tour/reader" type MyReader struct {}func (r MyReader) Read(data []byte ) (int ,error ){ for index := range data { data[index] = 'A' } return len (data),nil } func main () { reader.Validate(MyReader{}) } 输出结果: OK!
5.2. 练习:rot13Reader 有种常见的模式是一个 io.Reader 包装另一个 io.Reader,然后通过某种方式修改其数据流。
例如,gzip.NewReader 函数接受一个 io.Reader(已压缩的数据流)并返回一个同样实现了 io.Reader 的 *gzip.Reader(解压后的数据流)。
编写一个实现了 io.Reader 并从另一个 io.Reader 中读取数据的 rot13Reader,通过应用 rot13 代换密码对数据流进行修改。
rot13Reader 类型已经提供。实现 Read 方法以满足 io.Reader。
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 43 44 package mainimport ( "io" "os" "strings" ) type rot13Reader struct { r io.Reader } func (rot *rot13Reader) Read(b []byte ) (n int ,err error ){ n,err = rot.r.Read(b) for i:=0 ;i<n ;i++ { b[i] = rot13(b[i]) } return n,nil } func rot13 (b byte ) byte { var a byte switch { case b >= 'a' && b <= 'z' : a = 'a' case b >= 'A' && b <= 'Z' : a = 'A' default : return b } return (b-a+13 )%26 + a } func main () { s := strings.NewReader("Lbh penpxrq gur pbqr!" ) r := rot13Reader{s} io.Copy(os.Stdout, &r) } 输出结果: You cracked the code!
6. 图像 image 包定义了 Image 接口:
1 2 3 4 5 6 7 8 package imagetype Image interface { ColorModel() color.Model Bounds() Rectangle At(x, y int ) color.Color }
注意: Bounds 方法的返回值 Rectangle 实际上是一个 image.Rectangle,它在 image 包中声明。
(请参阅文档了解全部信息。)
color.Color 和 color.Model 类型也是接口,但是通常因为直接使用预定义的实现 image.RGBA 和 image.RGBAModel 而被忽视了。这些接口和类型由 image/color 包定义。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package mainimport ( "fmt" "image" ) func main () { p := image.NewNRGBA(image.Rect(0 ,0 ,100 ,100 )) fmt.Println(p.Bounds()) fmt.Println(p.At(0 ,0 ).RGBA()) } 输出结果: (0 ,0 )-(100 ,100 ) 0 0 0 0 0 0 0 0
6.1. 图像练习 还记得之前编写的图片生成器 吗?我们再来编写另外一个,不过这次它将会返回一个 image.Image 的实现而非一个数据切片。
定义你自己的 Image 类型,实现必要的方法并调用 pic.ShowImage。
Bounds 应当返回一个 image.Rectangle ,例如 image.Rect(0, 0, w, h)。
ColorModel 应当返回 color.RGBAModel。
At 应当返回一个颜色。上一个图片生成器的值 v 对应于此次的 color.RGBA{v, v, 255, 255}。
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 ( "golang.org/x/tour/pic" "image" "image/color" ) type Image struct {}func (i Image) Bounds() image.Rectangle { return image.Rect(0 , 0 , 50 , 100 ) } func (i Image) ColorModel() color.Model { return color.RGBA64Model } func (i Image) At(x, y int ) color.Color { return color.RGBA{100 , 200 , 255 , 255 } } func main () { m := Image{} pic.ShowImage(m) } 输出结果: