自定义类型

前面我们已经学习了不少基础和高级数据类型,在 Go 语言里面,我们还可以通过自定义类型来表示一些特殊的数据结构和业务逻辑。

使用关键字 type 来声明:

1
type NAME TYPE

声明语法

  • 单次声明
1
type City string
  • 批量声明
1
2
3
4
5
6
7
8
9
10
11
12
13
type (
B0 = int8
B1 = int16
B2 = int32
B3 = int64
)

type (
A0 int8
A1 int16
A2 int32
A3 int64
)

简单示例

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

type City string

func main() {
city := City("上海")
fmt.Println(city)
}

基本操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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")
}
}

总结: 自定义类型的原始类型的所有操作同样适用。

函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)))来修复。

不同自定义类型间的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
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) 错误,修复方法使用显式转换:

1
fmt.Println(int(height) / int(age))

声明和初始化

当我们第一次看到变量和声明时,我们只看了内置类型,比如整数和字符串。既然现在我们要讨论结构,那么我们需要把讨论范围扩展到指针。

创建结构的值的最简单的方式是:

1
2
3
4
5
6
7
goku := Saiyan{

Name: "Goku",

Power: 9000,

}

注意: 上述结构末尾的逗号 , 是必需的。没有它的话,编译器就会报错。你将会喜欢上这种必需的一致性,尤其当你使用一个与这种风格相反的语言或格式的时候。

我们不必设置所有或哪怕一个字段。下面这些都是有效的:

1
2
3
4
5
6
7
goku := Saiyan{}

// or

goku := Saiyan{Name: "Goku"}

goku.Power = 9000

就像未赋值的变量其值默认为 0 一样,字段也是如此。

此外,你可以不写字段名,依赖字段顺序去初始化结构体 (但是为了可读性,你应该把字段名写清楚):

1
goku := Saiyan{"Goku", 9000}

以上所有的示例都是声明变量 goku 并赋值。

许多时候,我们并不想让一个变量直接关联到值,而是让它的值为一个指针,通过指针关联到值。一个指针就是内存中的一个地址;指针的值就是实际值的地址。这是间接地获取值的方式。形象地来说,指针和实际值的关系就相当于房子和指向该房子的方向之间的关系。

为什么我们想要一个指针指向值而不是直接包含该值呢?这归结为 Go 中传递参数到函数的方式:镜像复制。知道了这个,尝试理解一下下面的代码呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {

goku := Saiyan{"Power", 9000}

Super(goku)

fmt.Println(goku.Power)

}

func Super(s Saiyan) {

s.Power += 10000

}

上面程序运行的结果是 9000,而不是 19000,。为什么?因为 Super 修改了原始值 goku 的复制版本,而不是它本身,所以,Super 中的修改并不影响上层调用者。现在为了达到你的期望,我们可以传递一个指针到函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 的值其实是一个地址。所以这个副本值也是一个与原值相等的地址,这就是我们间接传值的方式。想象一下,就像复制一个指向饭店的方向牌。你所拥有的是一个方向牌的副本,但是它仍然指向原来的饭店。

我们可以证实一下这是一个地址的副本,通过修改其指向的值(尽管这可能不是你真正想做的事情):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 值本身呢?