了解 Go 1.9 的类型别名

目录 [−]

  1. 类型别名
  2. 类型命名和类型声明的区别
  3. 类型循环
  4. 可导出性
  5. 方法集
  6. byte 和 rune 类型
  7. 参考资料

如你所知, 类型别名(type aliases) 最终还是加入到Go 1.9中, Go 1.9 beta2今天已经发布了, 正式版预计8月初发布, 是时候深入了解一下它的新特性了,本文介绍的就是它的重要的新特性之一: 类型别名。

当然,如果你想尝试这些新特性,需要安装Go 1.9的版本,目前是beta2版,可以在官方网站下载。

类型别名主要解决什么问题,为什么需要这个特性? Russ Cox 的论文Codebase Refactoring (with help from Go)介绍了它的背景。类型别名主要用在:

  1. 在大规模的重构项目代码的时候,尤其是将一个类型从一个包移动到另一个包中的时候,有些代码使用新包中的类型,有些代码使用旧包中的类型, 比如context
  2. 允许一个庞大的包分解成内部的几个小包,但是小包中的类型需要集中暴漏在上层的大包中

类型别名

类型别名的语法如下:

1
type identifier = Type

它和类型定义(type definition)类似,仅仅是在identifierType之间加了一个等号=,但是和类型定义区别很大,这一点会在后面专门比较。

下面这个例子就是为字符串string类型定义了一个别名S,你可以声明变量、常量为S类型,将字符串赋值给它,它和字符串类型几乎一模一样。

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
type S = string
func main() {
var s S = "hello world"
fmt.Println(s)
}

当然, 你可以为任意的类型定义类型别名,语言规范中没有限制,可以为数组、结构体、指针、函数、接口、Slice、Map、Channel定义别名,甚至你还可以为通过类型定义(type definition)的类型定义别名,更甚者是你可以为别名定义别名。

比如下面这个例子, 为函数类型func()定义了一个别名F:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
type F = func()
func main() {
var foo F = func() {
fmt.Println("hello type aliases")
}
foo()
}

又如下面的代码,为interface{}定义了别名G:

1
2
3
4
5
6
7
8
9
10
package main
import "fmt"
type G = interface{}
func main() {
var g G = "hello world"
fmt.Println(g)
}

类型别名还可以为其它包中的类型定义别名,只要这个类型在其它包中是exported的:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import (
"fmt"
"time"
)
type MyTime = time.Time
func main() {
var t MyTime = time.Now()
fmt.Println(t)
}

类型命名和类型声明的区别

记住下面一句话:

类型别名和原类型完全一样,只不过是另一种叫法而已

这句话隐藏着很多的智慧,你可以慢慢体会。

完全一样(identical types)意味着这两种类型的数据可以互相赋值,而类型定义要和原始类型赋值的时候需要类型转换(Conversion T(x))。

下面这个例子中,v是整数类型,可以直接赋值给d,因为d的类型是D,是是整数的别名。而var i I = v这一句会出错,因为I和整数是两个类型。

所以类型别名和类型定义最大的区别在于:类型别名和原类型是相同的,而类型定义和原类型是不同的两个类型。

1
2
3
4
5
6
7
8
9
10
package main
type D = int
type I int
func main() {
v := 100
var d D = v
var i I = v
}

比如类型定义type Tnamed Tunderlying,系列类型和组合类型是不同的:

  • TnamedTunderlying
  • *Tnamed*Tunderlying
  • chan Tnamedchan Tunderlying
  • func(Tnamed)func(Tunderlying)
  • interface{ M() Tnamed }interface{ M() Tunderlying }

但是对于别名type T1 = T2,下列类型和组合类型是相同的:

  • T1T2
  • *T1*T2
  • chan T1chan T2
  • func(T1)func(T2)
  • interface{ M() T1 }interface{ M() T2 }

还有一个重要的区别在于类型定义的类型的方法集和原始类型的方法集没有任何关系,而类型别名和原始类型的方法集是一样的,下面再介绍。

既然类型别名和原类型是相同的,那么在`switch - type中,你不能将原类型和类型别名作为两个分支,因为这是重复的case:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main
import "fmt"
type D = int
func main() {
var v interface{}
var d D = 100
v = d
switch i := v.(type) {
case int:
fmt.Println("it is an int:", i)
// case D:
// fmt.Println("it is D type:", i)
}
}

类型循环

类型别名在定义的时候不允许出现循环定义别名的情况,如下面所示:

1
2
type T1 = T2
type T2 = T1

上面的例子太明显,下面这个例子比较隐蔽,也是循环定义类型别名的情况,当然这些在编译代码的时候编译器会帮你检查,如果出现循环定义的情况会出错。

1
2
3
4
5
type T1 = struct {
next *T2
}
type T2 = T1

可导出性

如果定义的类型别名是exported (首字母大写)的,那么别的包中就可以使用,它和原始类型是否可exported没关系。也就是说,你可以为unexported类型定义一个exported的类型别名,如下面的例子:

1
2
3
4
5
type t1 struct {
S string
}
type T2 = t1

方法集

既然类型别名和原始类型是相同的,那么它们的方法集也是相同的。

下面的例子中T1T3都有saygreeting方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type T1 struct{}
type T3 = T1
func (t1 T1) say(){}
func (t3 *T3) greeting(){}
func main() {
var t1 T1
// var t2 T2
var t3 T3
t1.say()
t1.greeting()
t3.say()
t3.greeting()
}

如果类型别名和原始类型定义了相同的方法,代码编译的时候会报错,因为有重复的方法定义。

另一个有趣的现象是 embedded type, 比如下面的例子, T3T1的别名。在定义结构体S的时候,我们使用了匿名嵌入类型,那么这个时候调用s.say会怎么样呢? 实际是你会编译出错,因为s.say`不知道该调用s.T1.say还是s.T3.say`,所以这个时候你需要明确的调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
type T1 struct{}
type T3 = T1
func (t T1) say(){}
type S struct {
T1
T3
}
func main() {
var s S
s.say()
}

进一步想,这样是不是我们可以为其它库中的类型增加新的方法了, 比如为标准库的time.Time增加一个滴答方法:

1
2
3
4
5
6
7
8
9
type NTime = time.Time
func (t NTime) Dida() {
fmt.Println("嘀嗒嘀嗒嘀嗒嘀嗒搜索")
}
func main() {
t := time.Now()
t.Dida()
}

答案是: NO, 编译的时候会报错: cannot define new methods on non-local type time.Time

byte 和 rune 类型

在Go 1.9中, 内部其实使用了类型别名的特性。 比如内建的byte类型,其实是uint8的类型别名,而rune其实是int32的类型别名。

1
2
3
4
5
6
7
8
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

参考资料

  1. https://github.com/golang/proposal/blob/master/design/18130-type-alias.md
  2. https://github.com/golang/go/issues/18130
  3. https://talks.golang.org/2016/refactor.article
  4. https://github.com/golang/go/issues/16339