专栏名称: 狗厂
目录
相关文章推荐
51好读  ›  专栏  ›  狗厂

【译】Go语言的类型系统

狗厂  · 掘金  ·  · 2018-05-03 03:04

正文

概览

本文涉及到下面的几个方面:

  • 声明新的用户自定义类型
  • 为类型添加行为
  • 何时用值类型何时用指针类型
  • 使用接口实现多态
  • 通过组合扩展和改变类型
  • 标识符的暴露与不暴露

Go语言是一种静态类型的编程语言。编译器总是需要知道程序中的每个值的类型是什么。编译器提前知道值的类型信息,可以帮助程序安全的处理这些值。 这样可以减少潜在的bug或内存的破坏,同时还有机会让编译器产生更有效的代码。

变量的值类型为编译器提供两条信息:

  1. 值的尺寸: 需要为值分配多少内存。
  2. 内存代表什么。

很多内置类型中,类型名同时包含了值尺寸和所代表的东西。 比如int64类型表示需要8个字节内存(64位), 代表的是整数值。 float32类型表示需要4个字节内存(32位), 代表的是IEEE-754浮点数。bool类型需要一个字节内存(8位), 代表的是布尔值true或false。

而有些类型具体代表什么,是和机器的代码架构相关的。 例如, int类型的值,尺寸可能是64位,也有可能是32位,具体要看所在机器的架构情况了。 还有一些和架构相关的其他类型, 例如Go语言中的所有引用类型都是和架构相关的。比较幸运的是,这些类型的值,在创建和处理的时候,你无需知道这些信息。但是编译器不知道这些信息的话,它就不能防止你做一些可能引起伤害程序本身或者运行机器的事情了。

自定义类型

Go语言支持自定义类型的声明。在声明新类型的时候,构建的声明为编译器提供值的尺寸和内存所代表信息, 这点和内置类型的工作方式类似。Go语言中有两种声明用于自定义类型的方法。 最常用的就是用关键词struct来创建组合类型。

struct(结构体)是由固定的独立字段组合起来声明的。 结构体中的每个字段都由已知类型来声明的,这些已知类型可以是内置类型,也可以是用户自定义类型。

type user struct {
    name string
    email string
    exp int
    privileged bool
}

上面就声明了一个结构体类型。声明以type关键词开头,然后是新类型的名字,最后是关键词struct。 这个结构体包含四个字段,它们都是内置类型。 你可以看这些字段如何组合在一起形成新的类型。 类型一旦声明好,就可以使用它创建值。

var bill user

上面我们通过关键词var创建一个名为bill的user类型变量。当声明变量的时候,代表变量的值总是被初始化的。 可以使用特定值或对应类型的零值(变量类型的默认值)来初始化它们的值。

数字类型的零值是0。字符串的零值是空字符串。布尔值的零值是false。

上面的结构体中,零值需要应用到结构体中的每个字段。

每当变量被创建并初始化为它的零值时,我们习惯使用var关键词。保留对关键词var的使用,表示变量被设置为零值的一种方式。如果我们需要将变量初始化为零值意外的值,我们可以使用短变量声明符后面带一个结构体字面量。
lisa := user{
    name: "Lisa",
    email: "[email protected]",
    ext: 124,
    privileged: true,
}

注意短变量声明符(:=)前面的变量不能是已经声明的变量。 结构体字面量和类型后面跟上打括号构成,结构体中的每个字段名和字段值以冒号分割,值后面必须跟一个逗号。

短变量声明符(:=)提供两个目的,声明变量和初始化变量。 根据操作符右边的类型信息,短变量声明符可以确定变量的类型。

既然我们创建并初始化了一个结构体类型,那么我们就可以使用结构体字面量来执行初始化。

结构体类型的结构体字面量可以接受两种格式的内容。 上面展示的是第一种,括号里边列出每个字段名和字段值,中间用冒号分割,然后在值后面加上逗号。 字段顺序可以随意排放。

第二种形式可以省略字段名,只用值来声明。 如下所示:

lisa := user{"Lisa", "[email protected]", 123, true}

这种形式的值也可以分成多行列出, 但是上面这种形式的传统值都是放一行里边的,结尾没有逗号。这种情况下, 值的顺序就非常重要了,需要匹配结构体声明中的字段顺序。

当声明结构体类型是,不限制仅使用内置类型。你还可以使用一些使用其他自定义类型的字段。

type admin struct {
    person user
    level string
}

上面我们定义了一个新的admin结构体类型。这个结构体类型有一个名字为person的字段,类型为user, 另外还有一个string类型的level字段。创建这样的一个变量,初始化该类型时结构体字面量稍有变化。

// Declare a variable of type admin.
fred := admin{
    person: user{
        name: "Lisa",
        email: "[email protected]",
        ext: 123,
        privileged: true,
    },
    level: "super",
}

要初始化person字段,需要创建一个user类型的值。这就是上面我们的lisa变量的字面量。 使用结构体字面量形式,user类型的值被创建并赋值给person字段。

另外一种声明自定义类型的方式是使用现有类型,让现有类型作为类型的类型规范。在新类型可以用现有类型表示的情况中,这种申明方式非常有用。标准库中就有很多使用这种声明方式从内置类型创建高级类别功能的例子。

type Duration int64

上面就是标准库time中声明Duration类型的代码。Duration代表的是持续的纳秒时间。这个类型代表的是内置类型int64。 Duration和int64是两个有区别的、不同的类型。

为了更好的阐明这一点,我们可以看看下面这个不能编译的小程序。

package main

type Duration int64

func main() {
    var dur Duration
    dur = int64(1000)
}

// prog.go:7: cannot use int64(1000) (type int64) as type Duration in assignment

编译器清楚的知道问题是什么。 int64类型的值不能用于类型Duration. 换句话说,即便类型int64是Duration的基础类型, Duration仍然属于它自己的唯一类型。不同类型的值不能互相赋值, 即便它们能兼容。编译器不能隐式转换不同类型的值。

方法

方法提供了一种为用户自定义类型添加行为的方式。方法实际上就是函数,在关键词func和函数名之间包含了一个额外参数。

// Sample program to show how to declare methods and how the Go
// compiler supports them.

package main

import "fmt"

// user defines a user in the program.
type user struct {
    name string
    email string
}

// notify implements a method with a value receiver.

func (u user) notify() {
    fmt.Printf("Sending User Email to %s<%s>\n", u.name, u.email)
}
// changeEmail implements a method with a pointer receiver.
func (u *user) changeEmail(email) {
    u.email = email
}

// main is the entry point for the application.
func main() {
    // Values of type user can be used to call methods
    // declared with a value receiver.
    bill := user{"Bill", "[email protected]"}
    bill.notify()

    // Pointers of type user can also be used to call methods
    // declared with a value receiver.
    lisa := &user{"Lisa", "[email protected]"}
    lisa.notify()

    // Values of type user can be used to call methods
    // declared with a pointer receiver.
    bill.changeEmail("[email protected]")
    bill.notify()
 
    // Pointers of type user can be used to call methods
    // declared with a pointer receiver.
    lisa.changeEmail("[email protected]")
    lisa.notify()
}

上面展示了两个不同的方法。在关键词func和函数名之间的参数叫做接受者(receiver), 函数被绑定给这个特定的类型。 当函数有接受者时, 函数就被叫做方法。 当你运行上面的代码,会有下面的输出:

Sending User Email To Bill<[email protected]>
Sending User Email To Lisa<[email protected]>
Sending User Email To Bill<[email protected]>
Sending User Email To Lisa<[email protected]>

让我们检查下程序做了些什么。程序声明了结构体user, 然后声明了一个名为notify的方法。

type user struct {
    name string
    email string
}

func (u user) notify() {
    // ...
}

在Go语言中有两种类型的接收者: 值接受者和指针接受者。notify方法以值接受者的方式声明的。

notify方法的接收者被声明为类型user的值。 当以值接受者声明方法时,这个方法是种能与用于调用该方法的值副本进行操作。

bill := user{"Bill", "[email protected]"}
bill.notify()

上面使用user类型的值bill对方法notify进行调用。

这个语法看起来类似于调用包的函数。然而这个例子中,bill不是包名,而是一个变量名。 这种情况下我们调用notify方法, bill的值对于调用来说是接受者值, notify方法是对这个值的副本进行操作的。

你也可以使用指针来调用使用值接受者声明的方法。

lisa := &user{"Lisa", "[email protected]"}
lisa.notify()

上面我们使用user类型的指针lisa对方法notify()进行调用。为了支持方法调用,Go语言调整了指针以满足方法的接受者。你可以想象Go语言执行了下面的操作:

(*lisa).notify()

上面就展示了Go编译器所作的支持方法调用的等价。 指针值会被取消引用,以便方法调用和值接受者兼容。 再来一次,notify是操作副本的, 但是这次值的副本是lisa指针指向的。

同样可以使用指针接受者声明方法:

func (u *user) changeEmail(email string) {
    u.email = email
}

上面声明了changeEmail方法,使用的是指针接受者。这次,接受者不是user类型的值,而是指针。 当调用以指针接受者声明的方法时,用于调用方法的值是方法共享的。

lisa := &user{"Lisa", "[email protected]"}
lisa.changeEmail("[email protected]")

上面你看到lisa指针的声明,后面跟着changeEmail的方法调用。一旦changeEmail方法调用返回,对lisa指向的值的改变在调用后会受影响。这多亏了指针接受者。 值接受者操作的是用于方法调用的值的副本。而指针接受者操作的是实际的数据。







请到「今天看啥」查看全文