码迷,mamicode.com
首页 > 其他好文 > 详细

gopl 方法和接口

时间:2019-05-04 17:31:49      阅读:117      评论:0      收藏:0      [点我收藏+]

标签:min   就会   网络   动态   字节   防御   byte   断言   状态   

方法声明

写一个简单的方法:

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)

使用接口的一些建议

不要一开始就定义接口,每个接口却只是一个单独的实现。这种接口是不必要的抽象,还会有运行时的成本。仅在有两个或多个具体类型需要按统一的方式处理时才需要接口。
上面的建议也有特例,如果接口和类型实现出于依赖的原因不能放在同一个包里边,那么一个接口只有一个具体类型实现也是可以的。在这种情况下,接口是一种解耦两个包的好方式。

gopl 方法和接口

标签:min   就会   网络   动态   字节   防御   byte   断言   状态   

原文地址:https://blog.51cto.com/steed/2388824

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!