返回首页 - Notes - 2017

Go 语言类型系统


类型方法调用

为一个类型定义方法时,真正有所区别的地方只在于该方法要求接收者是值还是指针,至于实际调用者是值还是指针,无所谓,Go 会自动进行转换

接收者不同带来的影响在于:

示例如下:

package main

import "fmt"

type User struct {
  name string
  age  int
}

// 接收者为值,不会改变接收者本身
func (u User) change1(age int) {
  u.age = age
}

// 接收者为指针,会改变接收者本身
func (u *User) change2(age int) {
  u.age = age
}

func main() {
  user1 := User{"张三", 18}  // 值
  user2 := &User{"李四", 18} // 指针

  user1.change1(20) // 正常调用
  user2.change1(20) // 相当于调用 (*user2).change1(20)

  fmt.Println(user1.age) // 还是 18
  fmt.Println(user2.age) // 还是 18

  user1.change2(20) // 相当于调用 (&user1).change2(20)
  user2.change2(20) // 正常调用

  fmt.Println(user1.age) // 变成了 20
  fmt.Println(user2.age) // 变成了 20
}

同时也可以注意到,上面的例子中,user1 是值,user2 是指针,但访问两者的属性时写法一样,都写成 userx.age,这里也是一样的原理,Go 自动进行了值和指针的转换

有了这层自动转换,写代码就灵活多了,也没了那么多心智负担,相比 C/C++ 的指针操作而言,轻松了太多

但需要注意,普通函数传参时,参数要求是值就得传值,参数要求是指针就得传指针,这里 Go 不会进行转换,自动转换仅限于接收者


接口方法调用

实现接口的方法时,指定的接收者是值还是指针,对调用者的要求有区别,不会像上面所述的 类型方法调用 一样自行转换

方法接收者为值的示例:

package main

import "fmt"

type Hello interface {
  sayHello()
}

type User struct {
  name string
}

// 方法接收者为值
func (u User) sayHello() {
  fmt.Printf("Hello %s\n", u.name)
}

func testInterface(h Hello) {
  h.sayHello()
}

func main() {
  u1 := User{"张三"}
  u2 := &User{"李四"}

  testInterface(u1) // 正常调用
  testInterface(u2) // 正常调用
}

方法接收者为指针的示例:

package main

import "fmt"

type Hello interface {
  sayHello()
}

type User struct {
  name string
}

// 方法接收者为指针
func (u *User) sayHello() {
  fmt.Printf("Hello %s\n", u.name)
}

func testInterface(h Hello) {
  h.sayHello()
}

func main() {
  u1 := User{"张三"}
  u2 := &User{"李四"}

  testInterface(u1) // 编译出错,得写成这样 testInterface(&u1)
  testInterface(u2) // 正常调用
}

类型嵌套

可以在新类型里面嵌入已有的类型,成为 嵌入类型,嵌入类型具有的属性和方法会自动提升到父类型上面,就像父类型自己就有一样

嵌入类型 的写法是直接在新类型里面起一行,写该嵌入类型的名字即可,不要加其他的东西,不然就不是 嵌入类型

假如已有类型 User,类型 Admin 需要将 User 嵌入其中,下面是正确和错误的写法:

嵌入类型 属性和方法提升的示例:

package main

import "fmt"

type User struct {
  name string
}

func (u User) hello() {
  fmt.Printf("Hello %s\n", u.name)
}

type Admin struct {
  User
  level int
}

func main() {
  admin := Admin{User{"张三"}, 3}

  admin.User.hello() // Hello 张三
  admin.hello()      // 方法可以省略嵌入类型名直接调用

  fmt.Println(admin.User.name) // 张三
  fmt.Println(admin.name)      // 属性同样可以省略嵌入类型名直接调用
}

由于父类型自动具备了其 嵌入类型 的属性和方法,所以如果有函数需要指定的参数,就算父类型自己不符合参数的要求,只要其嵌入类型符合要求就可以直接传递父类型的变量,示例如下:

package main

import "fmt"

type Hello interface {
  hello()
}

type User struct {
  name string
}

func (u User) hello() {
  fmt.Printf("Hello %s\n", u.name)
}

type Admin struct {
  User
  level int
}

func sayHello(h Hello) {
  h.hello()
}

func main() {
  admin := Admin{User{"张三"}, 3}

  sayHello(admin.User) // Hello 张三
  sayHello(admin)      // sayHello 函数需要一个实现了 Hello 接口的参数,可以直接传递 Admin 这个父类型
}

如果父类型和其 嵌入类型 具有同名的属性或方法,则在父类型的变量上调用时,会覆盖从 嵌入类型 提升而来的属性和方法

package main

import "fmt"

type Hello interface {
  hello()
}

type User struct {
  name string
}

func (u User) hello() {
  fmt.Printf("Hello %s\n", u.name)
}

type Admin struct {
  User
  name  string
  level int
}

func (a Admin) hello() {
  fmt.Printf("Hello %s (from Admin)\n", a.name)
}

func sayHello(h Hello) {
  h.hello()
}

func main() {
  admin := Admin{User{"张三"}, "李四", 3}

  sayHello(admin.User) // Hello 张三
  sayHello(admin)      // Hello 李四 (from Admin)
}

公开与私有

Go 中,凡是小写字母开头的变量、属性、类型、函数、方法等,都是其所在包的私有数据,只有大写字母开头的才能被其他包访问

一般习惯上编写一个叫 New() 的工厂函数,初始化一些不能直接被外部包访问的数据

关于访问权限的问题,Go 的规则很简单也很严格,小写字母开头的就是不能直接从外部包访问,简单粗暴

我觉得这个设计相当好,省了三个关键字,还减轻了程序员的心智负担


date:2017-06-22、2017-06-23