gopl 方法和接口

方法声明

写一个简单的方法:

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)

使用接口的一些建议

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

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

时间: 2024-10-03 21:53:34

gopl 方法和接口的相关文章

1.扩展方法2.接口的隐式实现和显式实现

1.扩展方法:必须写在一个静态类里面,具体见代码: ? 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 namespace ConsoleApplication1 {     class Program     {         static void Main(string[] args)         {             Student s = new Stud

Go 方法和接口

方法 Go 没有类.然而,仍然可以在结构体类型上定义方法. 方法接收者 出现在 func 关键字和方法名之间的参数中. package main import ( "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 main() { v := &Vertex{

如何使用==操作符,Equals方法,ReferenceEquals方法,IEquatable接口比较2个对象

"世界上不会有两片完全相同的树叶",这句话适用于现实世界.而在软件世界中,这句话变成了"世界上必须有两片完全相同的树叶",否则,很多事情无以为继. 当比较2个对象是否相等时,通常情况下:==操作符用来比较值类型,比较的是值:实例方法Equals比较引用类型,比较的是对象的地址.静态方法Object.ReferenceEquals既可以比较引用类型也可以比较值类型,但比较的是对象的地址. 在实际项目中,当比较2个引用类型对象时,我们的需求变为:通过依次比较2个对象的所

C#--结构、类与属性、方法、接口、抽象与封密

C#--结构.类与属性 C#--方法 C#--接口.抽象与封密 原文地址:https://www.cnblogs.com/macT/p/9288188.html

初识 go 语言:方法,接口及并发

目录 方法,接口及并发 方法 接口 并发 信道 结束语 前言: go语言的第四篇文章,主要讲述go语言中的方法,包括指针,结构体,数组,切片,映射,函数闭包等,每个都提供了示例,可直接运行. 方法,接口及并发 方法 方法就是一类带特殊的接收者(struct)参数的函数 通过 结构体.方法 调用 示例: type city struct { name, address string } func (c city) sysCity() { c.name = "帝都" fmt.Println

反射之获取类,属性,方法,接口,父类等

获取类有三种方法 实体类和接口 public interface Person { public void sayHi(); } public class Student  implements Person{ private String id; private String name; private int age; public int sex=1; /*get和set方法省略*/ } Class c1 = Student.class; Class c2=Class.forName("c

抽象类、虚方法、接口的区别

接口 1.接口只提供方法规约,不提供方法体: 2.接口中的方法不能用关键字修饰: 3.接口里不能有接口和变量: 4.接口里的方法在子类中必须全部实现: 5.接口可以实现多重继承: 抽象类 1.抽象类可以从接口继承: 2.抽象类中的实体方法在子类中不可以重写,只可以被引用: 3.抽象类中的抽象方法不可以有方法体,抽象类中的抽象方法在子类中必须重写: 4.抽象类中的虚方法在子类中可以选择性的重写: 虚方法 1.可以在子类选择性的重写: 2.不重写也可被子类调用: 接口与抽象类的异同 1.抽象类的抽象

Spring回调方法DisposableBean接口

除了自定义的destroy-method.还可以实现DisposableBean接口,来回调bean销毁时候执行的方法,这个接口有一个destroy方法,生命周期是是destroy----bean销毁---自定义的destroy方法 SimpleBean.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 package ch5.destroy

记一次使用utl_http方法调用接口,报字符或值错误

背景:ebs系统和其他系统通过utl_http包调用接口,使用log方法记录日志. 某次调用接口,执行到记录日志行报字符或值错误. 查找原因,发现是p_str的长度超过的32747的限制. 解决办法: PROCEDURE log(p_str VARCHAR2) IS BEGIN fnd_file.put_line(fnd_file.log, p_str); dbms_output.put_line(p_str); END; --解决l_messge_clob长度超过3276导致的溢出问题,字符或