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