go的类型以及方法

go自称是一门简单安全的语言,是新时代的 c 语言, not c++ 。

声明新的类型

go 除了内置的整型、浮点型、复数和字符串之外,还可以灵活定义其他类型,方式也很简单:

  1. type Length uint64
  2. // var len Length = Length(20)
  3.  
  4. type People struct {
  5.         age  int
  6.         name string
  7. }
  8. // ant := People{age: 20, name: "ant"}

go 除了三种特殊的类型 slice、map 和 chan 是引用类型之外,其他的都是值类型。也就是说整型、浮点型、复数、字符串以及 struct 和指针等都是value,它们的赋值都是value拷贝。

方法

go 可以在新的类型上添加方法,实际就是函数,譬如:

  1. func (len Length) print() {
  2.         fmt.Println(len)
  3. }
  4. var len Length = Length(20)
  5. len.print()
  6. (&len).print() // ok, 因为 *T 的方法包含 T 中的方法

这非常像是面向对象中的方法调用,其实这样说也没有问题。在 go 中函数前面指定的 (v T) 是方法的接收器,表明哪种类型可以调用这个方法,就像上面的 Length 类型的 len 可以 len.print()。以类型 T 或是 *T 作为接收器的方法的集合叫做该类型的方法集, 而 *T 的方法集包含 T 的方法集。也就是 (&len).print() 也是合法的。而反过来说不成立

  1. func (len *Length) print2() {
  2.         fmt.Printf("%T:%v\n", *len, *len)
  3. }
  4. var len Length = Length(20)
  5. len.print2() // ??? why,不是因为 T 也有 print2() 方法,而是 go 会主动转换成 (&len).print2() 调用
  6. (&len).print2()

上面两个例子看上去不管是 T 作为接收器,还是 *T 作为接收器的方法,结构体 v.call() 和 (&v).call() 都可以使用。那一个方法是声明称 T 还是 *T 作为接收器呢?

  1. package main
  2.  
  3. import (
  4.         "fmt"
  5. )
  6.  
  7. type People struct {
  8.         age int
  9. }
  10.  
  11. func (p People) addAge1(age int) {
  12.         p.age += age
  13. }
  14.  
  15. func (p *People) addAge2(age int) {
  16.         p.age += age // ==> (*p).age += age
  17. }
  18.  
  19. func main() {
  20.         var people People = People{age: 20}
  21.         people.addAge1(10)    // ==> people.addAge1()
  22.         fmt.Println(people)   // 20
  23.         (&people).addAge1(10) // ==> (*(&people)).addAge1() ==> people.addAge1()
  24.         fmt.Println(people)   // 20
  25.  
  26.         people.addAge2(10)    // ==> (&people).addAge2()
  27.         fmt.Println(people)   // 30
  28.         (&people).addAge2(10) // (&people).addAge2()
  29.         fmt.Println(people)   // 40
  30. }

仔细观察会发现,*T 作为接收器的方法可以修改接收器(指向)对象的数据,而 T 作为接收器的方法不会。可以想象得到,*T 方法调用是传递的指针, T 方法调用是传递的值,数据拷贝方式不一样,代价自然也不一样。

so,可以简单归纳一下:

  • 如果方法调用想修改 T 中数据的值,那么用 *T 作为接收器;
  • 如果 T 是复杂的大对象,拷贝数据的成本很大,那么用 *T 作为接收器;
  • 如果方法调用不想修改 T 中数据的值,并且 T 是小对象(复制成本低),那么用 T 作为接收器;
  • 如果希望将 T v 作为 f(interface Interface) 的参数,也就是 f(v),那么必须是 T 作为接收器实现了接口 Interface, 而不能是 *T, 因为前面说了,T 的方法集不包含 *T 的。

方法的类型

方法也是有类型的,以 T 为接收器的方法 func (t T) add(delta int); 的类型是:func add(t T, delta int); 也就是:方法的类型是一个函数类型,接收器放在方法参数前作为第一个参数

实际上 golang 中的方法调用,完全是第一个参数为接收器调用的语法糖,更符合 OO 的写法,当然也更便利。var t T; t.add(10); 完全可以如此使用 T.add(t, 10); 进行调用,或是 (*T).add(&i, 10);

接口

golang 借鉴了 OO 的一重要 idea:面向接口编程。在 golang 中声明接口很简单:

  1. type Lock interface {
  2.         Lock()
  3.         Unlock()
  4. }
  5. type File interface {
  6.         Lock      // 和一一枚举 Lock 中的方法效果一样
  7.         Close()
  8. }

上面是声明了一个接口 Lock,带有两个方法:Lock() 和 Unlock();File 接口则是在 Lock 接口之上又添加了 Close() 方法。 Java 中接口扩展是通过 extends 来实现的, golang 中直接加进去就好了。

实现接口

golang 采用了一种叫做 duck 类型的模式:“有一个动物,如果它走的样子像鸭子,叫声也像鸭子,那么它就是鸭子。” 如果一个类型 T 或是 *T 的方法集包含了接口 Lock 中的所有方法,那么我们就说 T 或是 *T 实现了接口 Lock,那么对应的值 value 就可以赋值给 Lock: var lock Lock = t; 调用 lock.Lock() 就如同调用 t.Lock() 方法。

如同“空集是任何集合的子集”,空接口 interface{} 被所有类型实现了,那么 var i interface{} 可以接受任意类型,相当于 c 中的 void* 。

面向接口编程

这就是所谓的“面向接口编程”啊…… 保持接口的相对稳定。

值得一提的是,接口 I 可以作为函数参数,接口指针 *I 当然也可以。不过最好使用 I 而不要 *I:如果 T 实现了接口 I,那么 var i I = t; 是可以的,var i I = &t; 也是可以的,而 var i *I = &t; 是不可以的。因为 golang 中的指针是 value 类型,而 *interface{} 和 *T 是两种(结构)完全不同的类型,不能进行赋值;这和 c++ 是不一样的(c++中子类的指针或是值可以赋值给父类的指针和引用)。

参考出处

Tags: 

Article type: