类型别名是Golang 1.9引入的新特性,顾名思义类型别名是给Golang的类型提供创建别名的方法。使用的语法如下:
1 2 | type AliasType = SomeType //给SomeType 创建别名AliasType |
设计初衷
类型别名的设计初衷是为了解决代码重构时,类型在包(package)之间转移时产生的问题(参考 Codebase Refactoring (with help from Go) )。
考虑如下情景:
项目中有一个叫p1
的包,其中包含一个结构体T1
。随着项目的进展T1
变得越来越庞大。我们希望通过代码重构将T1
抽取并放入到独立的包p2
,同时不希望影响现有的其他代码。这种情况下以往的go语言的功能不能很好的满足此类需求。类型别名的引入可以为此类需求提供良好的支持。
首先我们可以将T1相关的代码抽取到包p2中:
1 2 3 4 5 6 7 8 9 | package p2 type T1 struct { ... } func (*T1) SomeFunc() { ... } |
之后在p1中放入T1的别名
1 2 3 4 5 | package p1 import "p2" type T1 = p2.T1 |
通过这种操作我们可以在不影响现有其他代码的前提下将类型抽取到新的包当中。现有代码依旧可以通过导入p1
来使用T1
。而不需要立马切换到p2
,可以进行逐步的迁移。
此类重构发生不仅仅存在于上述场景还可能存在于以下场景:
- 优化命名:如早期Go版本中的
io.ByteBuffer
修改为bytes.Buffer
。 - 减小依赖大小:如
io.EOF
曾经放在os.EOF
,为了使用EOF必需导入整个os
包。 - 解决循环依赖问题。
与类似语法的比较
使用已有类型声明新类型
Golang的类型声明可以使用已有的类型来创建新的类型。
1 2 3 4 5 6 7 8 9 10 11 12 | type T1 struct { I int } type T2 T1 func main() { t := &T2{ I: 12, } fmt.Printf("%d\n", t.I) } |
从T1
创建的新的类型T2
可以使用T1
中声明的成员变量I
,但是无法使用在T1
上定义的方法。
1 2 3 4 5 6 7 8 9 | func (*T1) Hello() { fmt.Printf("hello\n") } func main() { t := &T2{} t.Hello() } |
我们在T1
上定义了Hello
方法。如果我们在T2
中调用Hello
将会得到错误:
1 2 | t.Hello undefined (type *T2 has no field or method Hello) |
可见此种方式定义的新类型只是与原类型拥有相同的数据定义,但是不共享方法。
使用匿名成员来创建类型
Golang中的结构体定义可以使用匿名成员,此时新建的结构体可以直接使用匿名成员的成员变量与方法,如同使用自身的成员变量和方法。这也是Golang实现的类似继承的机制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | type T1 struct { Name string } type T2 struct { T1 } func (t *T1) Hello() { fmt.Printf("hello %s\n", t.Name) } func main() { t := &T2{} t.Name = "world" t.Hello() //out: hello world } |
这种方法看似可以替代类型别名,但是依旧存在一些问题。其中最关键的问题是虽然T1和T2看起来拥有相同的成员变量与方法,但是实际上是完全不同的类型,在有明确类型指定的地方无法混用两种类型。比如有如下函数:
1 2 3 4 | func SayHello(t *T1) { t.Hello() } |
给SayHello
传入T1
类型的变量可以正常工作,但是如果传入T2
类型的方法则会报错:
1 2 | cannot use t (type *T2) as type *T1 in argument to SayHello |
以上两种方法不仅在函数调用上无法混用外,也无法互相赋值、无法互相使用类型断言(Type Assertion)等。
语法细节
类型别名的使用实例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | type T1 struct { Name string } func (t *T1) Hello() { fmt.Printf("hello %s\n", t.Name) } type T2 = T1 func main() { t := &T2{} t.Name = "world" t.Hello() //out: hello world fmt.Printf("%#v", t) //out: &main.T1{Name:"world"} } |
通过示例可以看出我们可以在T2
直接使用T1
的成员变量与方法。有趣的是最后t
变量本身的输出,我们得到了&main.T1{Name:"world"}
。这表明看起来我们声明了T2
,但是经过编译后T2
实际上会被替换为T1
,他只是T1
的一个字面上的别名。(看起来像C语言当中的#define T2 T1
)
我们尝试为T2
定义一个新方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | type T1 struct { Name string } func (t *T1) Hello() { fmt.Printf("hello %s\n", t.Name) } type T2 = T1 func (t *T2) Bye() { fmt.Print("Bye!") } func main() { t1 := &T1{} t1.Bye() //out: Bye! } |
为T2
定义新方法后我们可以看到T1
也得到了同样的方法。这就进一步说明T2
仅仅是T1
的一个字面上的别名,两者表达的是相同的类型。
最后因为类型别名仅仅是字面上的另一个类型的别名,我们无法为包含在其他包当中的方法创建别名后为其声明新的方法,也就是下面的做法会报错:
1 2 3 4 5 6 7 8 | import "bytes" type Buf = bytes.Buffer func (b Buf) Hello(){ //cannot define new methods on non-local type bytes.Buffer fmt.Print("hello") } |
参考文献
https://talks.golang.org/2016/refactor.article
https://github.com/golang/proposal/blob/master/design/18130-type-alias.md