转自:https://colobu.com/2017/06/26/learn-go-type-aliases/
目录 [−]
如你所知, 类型别名(type aliases) 最终还是加入到Go 1.9中, Go 1.9 beta2今天已经发布了, 正式版预计8月初发布, 是时候深入了解一下它的新特性了,本文介绍的就是它的重要的新特性之一: 类型别名。
当然,如果你想尝试这些新特性,需要安装Go 1.9的版本,目前是beta2版,可以在官方网站下载。
类型别名主要解决什么问题,为什么需要这个特性? Russ Cox 的论文Codebase Refactoring (with help from Go)介绍了它的背景。类型别名主要用在:
- 在大规模的重构项目代码的时候,尤其是将一个类型从一个包移动到另一个包中的时候,有些代码使用新包中的类型,有些代码使用旧包中的类型, 比如
context
- 允许一个庞大的包分解成内部的几个小包,但是小包中的类型需要集中暴漏在上层的大包中
类型别名
类型别名的语法如下:
1 | type identifier = Type |
它和类型定义(type definition)类似,仅仅是在identifier
和Type
之间加了一个等号=
,但是和类型定义区别很大,这一点会在后面专门比较。
下面这个例子就是为字符串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
,系列类型和组合类型是不同的:
Tnamed
和Tunderlying
*Tnamed
和*Tunderlying
chan Tnamed
和chan Tunderlying
func(Tnamed)
和func(Tunderlying)
interface{ M() Tnamed }
和interface{ M() Tunderlying }
但是对于别名type T1 = T2
,下列类型和组合类型是相同的:
T1
和T2
*T1
和*T2
chan T1
和chan 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 |
方法集
既然类型别名和原始类型是相同的,那么它们的方法集也是相同的。
下面的例子中T1
和T3
都有say
和greeting
方法。
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
, 比如下面的例子, T3
是T1
的别名。在定义结构体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 |
参考资料
- https://github.com/golang/proposal/blob/master/design/18130-type-alias.md
- https://github.com/golang/go/issues/18130
- https://talks.golang.org/2016/refactor.article
- https://github.com/golang/go/issues/16339