自定义类型
前面我们已经学习了不少基础和高级数据类型,在 Go 语言里面,我们还可以通过自定义类型来表示一些特殊的数据结构和业务逻辑。
使用关键字 type
来声明:
1 | type NAME TYPE |
声明语法
- 单次声明
1 | type City string |
- 批量声明
1 | type ( |
简单示例
1 | package main |
基本操作
1 | package main |
总结: 自定义类型的原始类型的所有操作同样适用。
函数参数
1 | package main |
当我们运行代码的时候会出现 ./main.go:11:10: cannot use middle (type Age) as type int in argument to printAge
的错误。
因为 printAge
方法期望的是 int 类型,但是我们传入的参数是 Age
,他们虽然具有相同的值,但为不同的类型。
我们可以采用显式的类型转换( printAge(int(primary))
)来修复。
不同自定义类型间的操作
1 | package main |
当我们运行代码会出现 ./main.go:12:21: invalid operation: height / age (mismatched types Height and Age)
错误,修复方法使用显式转换:
1 | fmt.Println(int(height) / int(age)) |
声明和初始化
当我们第一次看到变量和声明时,我们只看了内置类型,比如整数和字符串。既然现在我们要讨论结构,那么我们需要把讨论范围扩展到指针。
创建结构的值的最简单的方式是:
1 | goku := Saiyan{ |
注意: 上述结构末尾的逗号 , 是必需的。没有它的话,编译器就会报错。你将会喜欢上这种必需的一致性,尤其当你使用一个与这种风格相反的语言或格式的时候。
我们不必设置所有或哪怕一个字段。下面这些都是有效的:
1 | goku := Saiyan{} |
就像未赋值的变量其值默认为 0 一样,字段也是如此。
此外,你可以不写字段名,依赖字段顺序去初始化结构体 (但是为了可读性,你应该把字段名写清楚):
1 | goku := Saiyan{"Goku", 9000} |
以上所有的示例都是声明变量 goku 并赋值。
许多时候,我们并不想让一个变量直接关联到值,而是让它的值为一个指针,通过指针关联到值。一个指针就是内存中的一个地址;指针的值就是实际值的地址。这是间接地获取值的方式。形象地来说,指针和实际值的关系就相当于房子和指向该房子的方向之间的关系。
为什么我们想要一个指针指向值而不是直接包含该值呢?这归结为 Go 中传递参数到函数的方式:镜像复制。知道了这个,尝试理解一下下面的代码呢?
1 | func main() { |
上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super 修改了原始值 goku 的复制版本,而不是它本身,所以,Super 中的修改并不影响上层调用者。现在为了达到你的期望,我们可以传递一个指针到函数中:
1 | func main() { |
这一次,我们修改了两处代码。第一个是使用了 & 操作符以获取值的地址(它就是 取地址 操作符)。然后,我们修改了 Super 参数期望的类型。它之前期望一个 Saiyan 类型,但是现在期望一个地址类型 Saiyan,这里 X 意思是 指向类型 X 值的指针 。很显然类型 Saiyan 和 *Saiyan 是有关系的,但是他们是不同的类型。
这里注意到我们仍然传递了一个 goku 的值的副本给 Super,但这时 goku 的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。
我们可以证实一下这是一个地址的副本,通过修改其指向的值(尽管这可能不是你真正想做的事情):
1 | func main() { |
上面的代码,又一次地输出 9000。就像许多语言表现的那样,包括 Ruby,Python, Java 和 C#,Go 以及部分的 C#,只是让这个事实变得更明显一些。
同样很明显的是,复制一个指针比复制一个复杂的结构的消耗小多了。在 64 位的机器上面,一个指针占据 64 bit 的空间。如果我们有一个包含很多字段的结构,创建它的副本将会是一个很昂贵的操作。指针的真正价值在于能够分享它所指向的值。我们是想让 Super 修改 goku 的副本还是修改共享的 goku 值本身呢?