1.方法
在Go中没有类这一定义,不同于java和js。不过你可以为类型定义方法,和js中的对象中的某一项为函数的调用方式相似。
在Go中,方法是一类带特殊的接受者参数的函数。
方法接收者在它自己的参数列表内,位于func关键字和方法名中间
Go1func (v Vertex) Abs() float64 //类似于这样
示例:
Go1package main2 3import (4 "fmt"5 "math"6)7 8type Vertex struct {9 X, Y float6410}11 12func (v Vertex) Abs() float64 {13 return math.Sqrt(v.X*v.X + v.Y*v.Y)14}15 16func main() {17 v := Vertex{3, 4}18 fmt.Println(v.Abs())19}
在这个示例中,Abs 方法拥有一个名字为 v,类型为 Vertex 的接收者。
需要记住的是:方法只是一个带接收者参数的函数。现在这个 Abs 的写法就是个正常的函数,功能并没有什么变化。
同时,我们也能为非结构类型体,如基本类型等声明方法。
在示例中,我们声明了一个带 Abs 方法的数值类型 MyFloat。
你只能为在同一个包中定义的接收者类型声明方法,而不能为其它别的包中定义的类型 (包括 int 之类的内置类型)声明方法。
ps:也就是接收者的类型定义和方法声明必须在同一包内。
Go1package main2 3import (4 "fmt"5 "math"6)7 8type MyFloat float649 10func (f MyFloat) Abs() float64 {11 if f < 0 {12 return float64(-f)13 }14 return float64(f)15}16 17func main() {18 f := MyFloat(-math.Sqrt2)19 fmt.Println(f.Abs())20}
与此同时,你可以为指针类型的接收者定义方法,这意味着对于某类型 T,接收者的类型可以用 *T 的文法。 (此外,T 本身不能是指针,比如不能是 *int。)指针接收者的方法可以修改接收者指向的值。 由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
若使用值接收者,那么 Scale 方法会对原始 Vertex 值的副本进行操作。(对于函数的其它参数也是如此。)Scale 方法必须用指针接收者来更改 main 函数中声明的 Vertex 的值。
ps:要区分什么时候使用指针接收者还是使用值接收者其实只需要判断你的方法执行时是否需要改变调用者,如果需要,使用指针接收者以实现调用方法时对接受者的变更能体现到原调用者,反之则使用值调用者。
实际上,值接收者在调用方法时是创建一个副本,再对副本进行操作,如果方法的接收者是一个非常大的struct,每次调用接受者函数 都会形成struct的大副本,对性能有巨大的影响。
Go中默认带指针参数的函数必须接受一个指针,而接收者为指针的的方法被调用时,接收者既能是值又能是指针。
Go1var v Vertex2ScaleFunc(v, 5) // 编译错误!3ScaleFunc(&v, 5) // OK4 5var b Vertex6b.Scale(5) // OK7p := &b8p.Scale(10) // OK
对于语句 v.Scale(5) 来说,即便 v 是一个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。反之也一样,接受一个值作为参数的函数必须接受一个指定类型的值,而以值为接收者的方法被调用时,接收者既能为值又能为指针。
2.接口
接口在Go中是一种数据类型,提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态。
多态:多态是指代码可以根据类型的具体实现采取不同行为的能力。因为任何用户定义的类型都可以实现任何接口,所以通过不同实体类型对接口值方法的调用就是多态。
示例:
Go1package main2 3import (4 "fmt"5 "math"6)7 8type Abser interface {9 Abs() float6410}11 12func main() {13 var a Abser14 f := MyFloat(-math.Sqrt2)15 v := Vertex{3, 4}16 17 a = f // a MyFloat 实现了 Abser18 a = &v // a *Vertex 实现了 Abser19 20 // 下面一行,v 是一个 Vertex(而不是 *Vertex)21 // 所以没有实现 Abser。22 a = v23 24 fmt.Println(a.Abs())25}26 27type MyFloat float6428 29func (f MyFloat) Abs() float64 {30 if f < 0 {31 return float64(-f)32 }33 return float64(f)34}35 36type Vertex struct {37 X, Y float6438}39 40func (v *Vertex) Abs() float64 {41 return math.Sqrt(v.X*v.X + v.Y*v.Y)42}
接口也是值。可以像值一样进行传递。同时,接口值也能作为函数的参数和返回值。
在内部,接口值可以看做包含值和具体类型的元组:
(value, type)
接口值保存了一个具体底层类型的具体值。
接口值调用方法时会执行其底层类型的同名方法。
示例:
Go1package main2 3import (4 "fmt"5 "math"6)7 8type I interface {9 M()10}11 12type T struct {13 S string14}15 16func (t *T) M() {17 fmt.Println(t.S)18}19 20type F float6421 22func (f F) M() {23 fmt.Println(f)24}25 26func main() {27 var i I28 29 i = &T{"Hello"}30 describe(i)31 i.M()32 33 i = F(math.Pi)34 describe(i)35 i.M()36}37 38func describe(i I) {39 fmt.Printf("(%v, %T)\n", i, i)40}
就算接口内的具体值为 nil,方法仍然会被 nil 接收者调用。在一些语言中(说的就是你Java),这会触发一个空指针异常,但在 Go 中通常会写一些方法来优雅地处理它(如本例中的 M 方法)。
nil 接口值既不保存值也不保存具体类型,为 nil 接口调用方法会产生运行时错误,因为接口的元组内并未包含能够指明该调用哪个 具体 方法的类型。
如果直接定义一个接口并指定了零个方法,这个接口就是空接口。
空接口可以保存任何类型的值。空接口被用来处理未知类型的值。例如,fmt.Print 可接受类型为 interface{} 的任意数量的参数。
4.类型断言
类型断言为我们提供了一个访问接口具体值的方式。
excel1t := i.(T)
该语句的断言值i保存了具体类型T,并将底层类型为T的值赋予变量t。
若 i 并未保存 T 类型的值,该语句就会触发一个 panic。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
excel1t, ok := i.(T)
若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生 panic。
其实就是直接对该interface中的值进行一个类型的确认,确认成功就不会panic,如果和你需要确认的类型不相同将会触发一个panic。
5.类型选择
在go中类型选择是一种按顺序从几个类型断言中选择分支的结构。类型选择一般和switch语句非常相似,不过类型选择中的case是类型而不是值,他们针对给定接口所存储的值的类型进行比较。
Go1switch v := i.(type) {2case T:3 // v 的类型为 T4case S:5 // v 的类型为 S6default:7 // 没有匹配,v 与 i 的类型相同8}
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。
此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。
一个简单的练习:
Go1package main2 3import (4 "fmt"5 "strings"6)7 8type IPAddr [4]byte9 10// TODO: 为 IPAddr 添加一个 "String() string" 方法。11 12func (ip IPAddr) String() string {13 // 使用 strings.Join 将字节切片转换为点号分隔的字符串14 // 需要先将每个字节转换为字符串15 strParts := make([]string, len(ip))16 for i, octet := range ip {17 strParts[i] = fmt.Sprint(octet)18 }19 return strings.Join(strParts, ".")20}21 22func main() {23 hosts := map[string]IPAddr{24 "loopback": {127, 0, 0, 1},25 "googleDNS": {8, 8, 8, 8},26 }27 28 for name, ip := range hosts {29 30 fmt.Printf("%v: %v\n", name, ip)31 }32}
6.错误
Go 程序使用 error 值来表示错误状态。与fmt.Stringer 类似,error 类型是一个内建接口:
Go1type error interface {2 Error() string3}
(与 fmt.Stringer 类似,fmt 包也会根据对 error 的实现来打印值。)
通常函数会返回一个 error 值,调用它的代码应当判断这个错误是否等于 nil 来进行错误处理。
Go1i, err := strconv.Atoi("42")2if err != nil {3 fmt.Printf("couldn't convert number: %v\n", err)4 return5}6fmt.Println("Converted integer:", i)
error 为 nil 时表示成功;非 nil 的 error 表示失败。