方法声明
写一个简单的方法:
type Point struct{X, Y float64}
// 普通的函数
func Distance(p, q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
// 同样的作用,用方法实现
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
接收者:附加的参数 p 称为方法的接收者。
调用方法的时候,接收者在方法名的前面。这样就和声明保持一致:
p := Point{1, 2}
q := Point{4, 6}
fmt.Println(Distance(p, q)) // 函数调用
fmt.Println(p.Distance(q)) // 方法调用
选择子:表达是 p.Distance 称作选择子(selector),因为它为接收者 p 选择合适的 Distance 方法。
指针接收者的方法
对于函数,它会复制每一只实参变量。如果函数需要更新一个变量,或者是因为实参太大而需要避免复制整个实参,就需要使用指针来传递变量的地址。
对于方法的接受者,也可以将方法绑定到指针类型。习惯上遵循如果一个类型的任何一个方法使用指针接收者,那么所有该类型的方法都应该使用指针接收者,即使有些方法不一定需要。
另外,为了防止混淆,不允许本身是指针的类型进行方法声明,会有编译错误:
type p *int
func (p) f() { /*...*/ } // 编译错误:非法的接收者类型
方法变量与表达式
方法变量(method value)
通常是在相同的表达式里使用和调用方法,但是把两个操作分开也是可以的。选择子 p.Distance 可以赋予一个方法变量,它是一个函数,把方法(Point.Distance)绑定到一个接收者 p 上。函数只需要提供实参而不需要提供接收者就能够调用:
p := Point{1, 2}
q := Point{4, 6}
distanceFromP := p.Distance // 方法变量
fmt.Println(distanceFromP(q))
这里 p.Distance 是选择子,把它赋值给变量 distanceFromP,这个变量就是方法变量,并且这个变量是一个函数。
如果包内的 API 调用一个函数值,并且使用者期望这个函数的行为是调用一个特定接收者的方法,方法变量就非常有用。使用方法变量还可以是代码更加简洁:
type Rocket struct { /* ... */ }
func (r *Rocket) Launch() { /* ... */ }
r := new(Rocket)
time.AfterFunc(10 * time.Second, func() { r.Launch() }) // 如果没有方法变量,那么要把执行一个方法包在一个函数里,等到函数被调用后执行
time.AfterFunc(10 * time.Second, r.Launch) // 使用方法变量,这里 r.Launch 就是一个函数,只是没有赋值给某个变量,没有函数名
函数 time.AfterFunc 的作用是在指定的延迟后调用一个函数。上面说了,方法变量也是函数。
方法表达式(method expression)
调用方法的时候必须提供接收者,并且按照选择子的语法进行调用。
方法表达式,写成 T.f 或者 (*T.f)。
其中 T 是类型,是一种函数变量,把原来方法的接收者替换成函数的第一个形参,因此它可以像平常的函数一样调用:
p := Point{1, 2}
q := Point{4, 6}
distance := Point.Distance // 方法表达式
fmt.Println(distance(p, q))
fmt.Printf("%T\n", distance) // "func(Point, Point) float64"
如果需要一个值来代表多个方法中的一个,而方法都属于同一个类型,方法表达式可以实现让这个值所对应的方法来处理不同的接收者。就是可以把一个方法变成一个函数,函数的变量会增加一个,第一个变量就是原来方法中的接收者。其实各个参数的顺序还是一样的,原本第一个参数在 func 前,现在移动到了 func 后面。 p.Distance(q) 变成了 distance(p, q)。
接口类型
io包定义了很多有用的接口:
- io.Writer : 抽象了所有写入字节的类型,下面会列举
- io.Reader : 抽象了所有可以读取字节的类型
- io.Closer : 抽象了所有可以关闭的类型,比如文件或者网络连接
io.Writer 是一个广泛使用的接口,它负责所有可以写入字节的抽象,包括但不限于下面列举的这些:
- 文件
- 内存缓冲区
- 网络连接
- HTTP客户端
- 打包器(archiver)
- 散列器(hasher)
接口值
接口值,就是一个接口类型的值。分两个部分:
- 动态类型 : 该接口的具体类型
- 动态值 : 该具体类型的一个值
var w io.Writer // 声明接口,动态类型和动态值都是nil
w = os.Stdout // 有动态类型,也有动态值
w = io.Writer(os.Stdout) // 和上面这句等价,把一个具体类型显式转换为接口类型
w = new(bytes.Buffer) // 有动态类型,也有动态值
w = nil // 把动态类型和动态值都设置为nil,恢复到声明时的状态
比较接口值
接口值可以用 == 和 != 来比较。动态类型一致,然后动态值相等(使用动态类型的 == 来比较),那么接口值相等。接口值都是nil也是相等的。
可以作为map的key,也可以作为switch语句的操作数,因为可以比较。
动态值可能是不可比较的类型,比如切片。对这样的接口进行比较,就会Panic。把这样的接口用作map的key或者switch语句的操作数时也同样会Panic。所以,仅在能确认接口值包含的动态值可以比较时,才比较接口值。
fmt 包的 %T 打印出来的就是动态类型。在内部实现中,fmt 用反射来拿到接口动态类型的名字。
注意:含有空指针的非空接口
空的接口值(动态类型和动态值都为空)和仅仅动态值为nil的接口值是不一样的。
const debug = true
func main() {
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
if debug {
// ...使用 buf...
}
}
// 如果 out 不是 nil,那么会向其写入输出的数据
func f(out io.Writer) {
// ...其他代码...
if out != nil {
out.Write([]byte("done\n"))
}
}
这里,把一个类型为 *bytes.Buffer 的空指针赋给了 out 参数,此时 out 的动态值为空。但它的动态类型是 *bytes.Buffer。就是说 out 是一个包含空指针的非空接口,所以这里的检查 out != nil
是 true,防御不了这种情况。
对于某些类型,比如 *os.File,空接收值是合法的。但是对于这里的 *buyes.Buffer,要求接收者不能为空,于是运行时会Panic。
这里的解决方案是,把 main 函数中的 buf 类型修改为 io.Writer,从而避免在最开始就把一个功能不完整的值赋给一个接口:
var buf io.Writer
if debug {
buf = new(bytes.Buffer)
}
f(buf)
类型断言
类型断言是一个作用在接口值上的操作,代码类似于x(T)
,x是一个接口类型的表达式,而T是一个类型(称为断言类型)。类型断言会检查操作数的动态类型是否满足指定的断言类型。
这里有两种可能:
- 断言类型T是一个具体类型
- 断言类型T是一个接口类型
具体类型
如果断言类型T是一个具体类型,断言类型会检查x的动态类型是否就是T。如果检查成功,返回x的动态值,返回的类型就是T。如果检查失败,那么操作崩溃。
接口类型
如果断言类型T是一个接口类型,断言类型会检查x的动态类型是否满足T。如果检查成功,动态值并没有提取出来,仍然是一个接口值,接口值的类型和值部分也不会变,只是结果类型为接口类型T。就是说,这里类型断言就是一个接口值表达式,从一个接口类型变为拥有另外一套方法的接口类型,但保留了接口值中动态类型和动态值部分。如果检查失败还是会崩溃。
类型断言可以返回两个结果,此时操作不会因为检查失败而崩溃。多出来的返回值是一个布尔型,用来指示断言是否成功。按照惯例,一般变量名用ok。如果操作失败,ok为false,而第一个返回值会是断言类型的零值。
类型分支
接口有两种不同的风格。
第一种风格下,典型的比如:io.Reader、io.Writer、fmt.Stringer、sort.Interface、http.Handler 和 error。接口上的各种方法突出了满足这个接口的具体类型之间的相似性,但隐藏了各个具体类型的布局和各自特有的功能。这种风格强调了方法,而不是具体类型。
第二种风格则充分利用了接口值能够容纳各种具体类型的能力,它把接口作为这些类型的联合(union)来使用。类型断言用来在运行时区分这些类型并分别处理。这这种风格中,强调的是满足这个接口的具体类型,而不是这个接口的方法(经常是没变方法的空接口),也不注重信息隐藏。这种风格的接口使用方式称为可识别联合(discriminated union)。
如果对面向对象熟悉,这两种风格分别对应:
- 子类型多态(subtype polymorphism)
- 特设多态(ad hoc polymorphism)
使用接口的一些建议
不要一开始就定义接口,每个接口却只是一个单独的实现。这种接口是不必要的抽象,还会有运行时的成本。仅在有两个或多个具体类型需要按统一的方式处理时才需要接口。
上面的建议也有特例,如果接口和类型实现出于依赖的原因不能放在同一个包里边,那么一个接口只有一个具体类型实现也是可以的。在这种情况下,接口是一种解耦两个包的好方式。