类型系统
Go语言的类型系统可以分为命名类型、未命名类型、底层类型、动态类型和静态类型等。
1. 类型简介
1.1 命名类型和非命名类型
命名类型(Named Type)
类型可以通过标识符来表示,这种类型称为命名类型。Go语言的基本类型中有20个预声明简单类型都是命名类型,Go语言还有一种命名类型——用户自定义类型。
未命名类型(Unamed Type)
一个类型由预声明类型、关键字和操作符组合而成,这个类型称为未命名类型。又称为类型字面量(Type Literal)。
Go语言的基本类型中的复合类型:数组(array)、切片(slice)、字典(map)、通道(channel)、指针(pointer)、函数字面量(function)、结构(struct)和接口(interface)都属于类型字面量,都是未命名类型。
所以*int,[]int,[2]int,map[k]v都是未命名变量。
1.2 底层类型
- 所有“类型”都有一个underlying type(底层类型)。底层类型的规则如下:
- (1)预声明类型(Pre-declared types)和类型字面量(type literals)的底层类型是她们自身。
- (2)自定义类型type newtype oldtype中的newtype的底层类型是逐层递归向下查找的。
1.3 类型相同和类型赋值
类型相同
Go是强类型的语言,编译器在编译时会进行严格的类型检验。两个命名类型是否相同,参考如下:
(1)两个命名类型相同的条件是两个类型声明的语言完全相同。
(2)命名类型和未命名类型永远不相同
(3)两个未命名类型相同的条件是它们的类型声明字面量的结构相同,并且内部元素的类型相同。
(4)通过类型别名语句声明的两个类型相同。
类型可直接赋值
- 不同类型的变量之间一般是不能直接相互赋值的,除非满足一定的条件。
- 类型为T1的变量a可以赋值给类型为T2的变量b,称为类型T1可以赋值给类型T2,伪代码如下:
//a是类型T1的变量,或者a本身就是一个字面常量或nil
var b T2 = a
a可以赋值给变量b必须满足如下条件中的一个:
(1)T1和T2的类型相同
(2)T1和T2具有相同的底层类型,并且T1和T2里面至少有一个未命名类型。
(3)T2是接口类型,T1是具体类型,T1的方法集是T2方法集的超集
(4)T1和T2都是通道类型,它们拥有相同的元素类型,并且T1和T2中至少有一个是未命名类型。
(5)a是预声明标识符nil,T2是pointer、function、slice、map、channel、interfacce类型中的一个。
(6)a是一个字面量常量,可以用来表示类型T的值。
package main
import "fmt"
type Map map[string]string
func (m Map) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type iMap Map
//只要底层类型是slice、map等支持range的类型字面量,新类型仍然可以使用range迭代
func (m iMap) Print() {
for _, key := range m {
fmt.Println(key)
}
}
type slice []int
func (s slice) Print() {
for _, v := range s {
fmt.Println(v)
}
}
func main() {
mp := make(map[string]string, 10)
mp["hi"] = "tata"
//mp与ma有相同的底层类型map[string][string],并且mp是未命名类型
//所以mp可以直接赋值给ma
var ma Map = mp
// im与ma虽然有相同的底层类型map[string][string],但是它们中没有一个是未命名类型,所以不能赋值,
//var im iMap = ma
ma.Print()
//im.Print()
// Map实现了Print(),所以其可以赋值给接口类型变量
var i interface {
Print()
} = ma
i.Print()
s1 := []int{1, 2, 3}
var s2 slice
s2 = s1
s2.Print()
}
1.4 类型强制转换
- Go语言是强类型的语言,如果不满足自动转换的条件,则必须进行强制类型转换。
- 任何两个不相干的类型如果进行强制转换,必须符合一定的规则。
- 强制类型的语法格式:var a T= (T) (b),使用括号将类型和要转换的变量或表达式的值括起来
- 非常量类型的变量x可以强制转化并传递给类型T,需要满足如下任一条件:
- (1)x可以直接赋值给T类型变量
- (2)x的类型和T具体相同的底层类型。
- (3)x的类型和T都是未命名的指针类型,并且指针指向的类型具有相同的底层类型
- (4)x的类型和T都是整型,或者都是浮点型
- (5)x的类型和T都是复数类型
- (6)x是整数值或[]byte类型的值,T是string类型
- (7)x是一个字符串,T是[]byte或[]rune。
注意:
(1)数值类型和string类型之间的相互转换可能造成值部分丢失;其他的转换仅是类型的转换,不会造成值的改变。string和数字之间的转换可使用标准库strconv
。
(2)Go语言没有语言机制支持指针和integer之间的直接转换,可以使用标准库中的unsafe
包进行处理。
2. 类型方法
2.1 自定义类型
- 用户自定义类型使用关键字type,其语法格式是
type newtype oldtype
。oldtype可以是自定义类型、预声明类型、未命名类型中的任意一种。newtype是新类型的标识符,与oldtype具有相同的底层类型,并且都继承了底层类型的操作集合。 - newtype和oldtype是两个完全不同的类型,newtype不会集成oldtype的方法,无论oldtype是什么类型,使用type声明的新类型都是一种命名类型,也就是说,自定义类型都是命名类型。
自定义struct类型
- struct类型是Go语言自定义类型的普遍的形式,是Go语言类型扩展的基石。
- 当用户声明一个新类型时,这个声明就给编译器提供了一个框架,告知必要的内存大小和表示信息。
- 结构里每个字段都会用一个已知类型声明。这个已知类型可以是内置类型,也可以是其他用户定义的类型。
type Person struct{
name string
email string
age int
}
- (1)按照字段顺序进行初始化
- (2)指定字段名进行初始化
package main
import "fmt"
type Person struct {
name string
age int
}
func main() {
a := Person{"john", 18}
b := Person{
"Tom",
20,
}
c := Person{
"Jim",
21,
}
// 下面是推荐的三种方法
d := Person{name: "Kim", age: 20}
e := Person{
name: "Band",
age: 22,
}
f := Person{
name: "Pony",
age: 22,
}
fmt.Println(a)
fmt.Println(b)
fmt.Println(c)
fmt.Println(d)
fmt.Println(e)
fmt.Println(f)
}
- (3)使用new创建内置函数,字段默认初始化为其类型的零值,返回值是指向结构的指针。这种方法一般不常用,一般使用struct都不会将所有字段初始化为零值。
- (4)一次初始化一个字段
p := Person{}
p.name = "Tom"
p.age = 20
- (5)使用构造函数进行初始化。这是推荐的方法,当结构发生变化时,构造函数可以屏蔽细节。
结构字段的特点
- 结构的字段可以是任意的类型,基本类型、接口类型、指针类型、函数类型都可以作为struct的字段。
- 结构字段的类型名必须唯一,struct字段类型可以是普通类型,也可以是指针。
- 结构支持内嵌自身的指针,这也是实现树形和链表等复杂数据结构的基础。
import "container/list"
type Element struct {
// 指向自身类型的指针
next, prev *Element
list *list.List
Value interface{}
}
匿名方法
- 在定义struct的过程中,如果字段只给出字段类型,没有给出字段名,则称这样的字段为“
匿名字段
”。 - 被匿名嵌入的字段必须是命名类型或命名类型的指针,类型字面量不能作为匿名字段使用。
- 匿名字段的字段名默认就是类型名,如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。
- 但是一个结构体里面不能同时存在某一类型及其指针类型的匿名字段,原因是二者的字段名相等。如果嵌入的字段来自其他包,则需要加上包名,并且必须是其他包可以导出的类型。
自定义接口类型
- 自定义接口类型同样使用type关键字声明。
// interface{}是接口字面量类型标识,所以i是非命名类型变量
var i interface{}
//Reader是自定义接口类型,属于命名类型
type Reader interfacce{
Read(p []byte) (n int, err error)
}
2.2 方法
- Go语言的类型方法是一种对类型行为的封装。可以看作特殊类型的函数,其显式地将对象实例或指针作为函数的第一个参数,并且参数名可以自己指定。这个对象实例或指针称为方法的接收者(reciver)。
- 关键字 func 和函数名之间的参数被称作接收者,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为方法。
- Go 语言里有两种类型的接收者:
值接收者
和指针接收者
。
//类型方法接收者是值类型
func (t TypeName)MethodName(ParamList) (Returnlist) {
// method body
}
//类型方法接收者是指针
func (t *TypeName)MethodName(ParamList) (Returnlist) {
//method body
}
- t是接收者,可以自由指定名称。
- TypeName为命名类型的类型名
- MethodName为方法名,是一个自定义标识符
- ParamList是形参列表
- ReturnList是返回值列表。
- 当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值
- 值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法。
- 也可以使用一个值来调用使用指针接收者声明的方法
- 如果给这个类型增加或者删除某个值,是要创建一个新的值,还是要更改当前的值?如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。
- 内置类型是由语言提供的一组类型。我们已经见过这些类型,分别是
数值类型
、字符串类型
和布尔类型
。 - Go 语言里的引用类型有如下几个:
切片
、映射
、通道
、接口
和函数类型
。 - 是使用值接收者还是指针接收者,不应该由该方法是否修改了接收到的值来决定。这个决策应该基于该类型的本质。
- 这条规则的一个例外是,需要让类型值符合某个接口的时候,即便类型的本质是非原始本质的,也可以选择使用值接收者声明方法。这样做完全符合接口值调用方法的机制。
- Go语言的类型方法本质上就是一个函数,没有使用隐式的指针。
//将类型的方法改写成常规的函数
func TypName_MethodName(t TypeName, otherParamList) (Returnlist) {
//method body
}
func TypName_MethodName(t *TypeName, otherParamList) (Returnlist) {
//method body
}
- 类型方法的特点:
- (1)可以为命名类型增加方法(除了接口),非命名不能自定义方法。
- (2)为类型增加方法有一个限制,就是方法的定义必须和类型的定义在同一个包中。
- (3)方法的命名空间的可见性和变量一样,大写开头的方法可以在包外被访问,否则只能在包内可见。
- (4)使用type定义的自定义类型是一个新类型,新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承。
package main
import "fmt"
type Map map[string]string
func (m Map) Print() {
//底层类型支持range运算,新类型可用
for _, key := range m {
fmt.Println(key)
}
}
type MyInt int
func main() {
var a MyInt = 10
var b MyInt = 10
// int类型支持的加减乘除运算,新类型同样可用
c := a + b
d := a * b
fmt.Printf("%d\n", c)
fmt.Printf("%d\n", d)
}
3. 方法调用
3.1 一般调用
类型方法的一般调用方式:
TypeInstancceName.MethodName(ParamList)
- TypeInstanceName: 类型实例名或指向实例的指针变量名;
- MethodName:类型方法名
- ParamList:方法实参
package main
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func main() {
var t = &T{}
//普通方法调用
t.Set(2)
//普通方法调用
t.Get()
}
3.2 方法值(method value)
- 变量x的静态类型是T,M是类型T的一个方法,x.M被称为方法值(method value)。x.M是一个函数类型变量,可以赋值给其他变量,并像普通的函数名一样使用。
f := x.M
f(args...)
等价于
x.M(args ...)
方法值(method value)其实是一个带有闭包的函数变量,其底层实现原理和带有闭包的匿名函数类似,接收值被隐式地绑定到方法值(method value)的闭包环境中。后续调用不需要再显显式地传递接收者。
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p,%v,%d \n", t, t, t.a)
}
func main() {
var t = &T{}
// method value
f := t.Set
//方法值调用
f(2)
t.Print()
//方法值调用
f(3)
t.Print()
}
3.3 方法表达式(method expression)
- 方法表达式相当于提供一种语法将类型方法调用显式地转换为函数调用,接收者(receiver)必须显式地传递进去。
func (t T) Get() int {
return t.a
}
func (t *T) Set(i int) {
t.a = i
}
func (t *T) Print() {
fmt.Printf("%p,%v,%d \n", t, t, t.a)
}
表达式T.Get和(*T).Set被称为方法表达式(method expression),方法表达式可以看作函数名,只不过这个函数的首个参数是接收者的实例或指针。
Go的方法底层是基于函数实现的。
3.4 方法集(method set)
命名类型方法接收者有两种类型:一个是值类型
,另一个是指针类型
,前者的形参是值类型,后者的形参是指针类型。无论接收者是什么类型,方法和函数的实参传递都是值拷贝。
package main
import "fmt"
type Int int
func (a Int) Max(b Int) Int {
if a >= b {
return a
} else {
return b
}
}
func (i *Int) Set(a Int) {
*i = a
}
func (i Int) Print() {
fmt.Printf("value=%d\n", i)
}
func main() {
var a Int = 10
var b Int = 20
c := a.Max(b)
c.Print()
(&c).Print()
a.Set(20)
a.Print()
(&a).Set(30)
a.Print()
}
为了简化描述,将接收者(receiver)为值类型T的方法的集合记为S,将接收者(receiver)为指针类型T的方法的集合统称为S。类型的方法集总结如下:
(1)T类型的方法集为S
(2)T类型的方法集是S和S
3.5 值调用和表达式调用的方法集
- (1)通过类型字面量显式地进行值调用和表达式调用,可以看到在这种情况下编译器不会做自动转换,会进行严格的方法集检查。
- (2)通过类型变量进行值调用和表达式调用,在这种情况下,使用值调用(method value)方式调用时编译器会进行自动转换,使用表达式调用(method expression)方式调用时编译器不会进行转换,会进行严格的方法集检查。
4. 组合和方法集
结构类型(struct)为Go提供了强大的类型扩展,主要是两方面:第一,struct可以嵌入任意其他类型的字段;第二,struct可以嵌套自身的指针类型的字段。
4.1 组合
- 使用type定义的新类型不会继承原有类型的方法,有个特例就是命名结构类型,命名结构类型可以嵌套其他的命名类型的字段,外层的结构类型是可以调用嵌入字段类型的方法,这种调用既可以是显式的,也可以是隐式的,这就是Go的“继承”,也就是“组合”.
- struct类型中的字段称为“内嵌字段”。
内嵌字段的初始化和访问 - struct的字段访问使用点操作符“.”,struct的字段可以嵌套很多层,只要内嵌的字段是唯一的即可,不需要使用全路径进行访问。
- 在struct的多层嵌套中,不同嵌套层次可以有相同的字段,此时最好使用完全路径进行访问和初始化。
内嵌字段的方法调用
- 如果外层字段和内层字段有相同的方法,则使用简化模式访问外层的方法会覆盖内存的方法。即在简写模式下,Go编译器优先从外向内逐层查找方法,同名方法中外层的方法能够覆盖内层的方法。
- 不推荐在多层的struct类型中内嵌多个同名的字段;但是并不反对struct定义和内嵌字段同名方法的用法。
4.2 组合的方法集
组合结构的方法集有如下规则:
(1)若类型S包含匿名字段T,则S的方法集包含T的方法集
(2)若类型S包含匿名字段T,则S的方法集包含T和T方法集
(3)不管类型S中嵌入的匿名字段是T还是*T,S方法集总是包含T和T方法集。
- 编译器的自动转换仅适用于直接通过类型实例调用方法时才有效,类型实例传递给接口时,编译器不会进行自动转换,而是会进行严格的方法集检验。
5. 函数类型
- 使用func FunctionName()语法格式定义的函数称为“有名函数”.
- 所谓的匿名函数就是在定义时使用func()语法格式,没有指定函数名。
- 函数类型也分为两种,一种是函数字面量类型(未命名类型),另一种是函数命名类型。
函数字面量类型
函数字面量类型的语法表达式是func (InputTypeList)OutputTypeList,“有名函数”和“匿名函数”都属于函数字面量类型。
函数命名类型
使用type NewType OldType语法定义一种新类型,这种类型都是命名类型,同理可以使用该方法定义一种新类型:函数命名类型,简称函数类型
。
type NewFuncType FuncLiteral
NewFuncType为新定义的函数命名类型,FuncLiteral为函数字面量类型,FuncLiteral为函数类型NewFuncType的底层类型,当然也可以使用type在一个函数类型中再定义一个新的函数类型。
函数签名
所谓的“函数签名”,就是“有名函数”和“匿名函数”的字面量类型。不包括函数名。
函数声明
Go代码调用Go编写的函数不需要声明,可以直接调用,但是Go调用汇编语言编写的函数还是要使用函数声明语句。
//函数声明=函数名+函数签名
//函数签名
func (InputTypeList)OutputTypeList
//函数声明
func FuncName (InputTypeList)OutputTypeList
上述概念的示例:
package main
import "fmt"
//有名函数定义,函数名为add
func add(a, b int) int {
return a + b
}
//函数声明语句,用于Go代码调用汇编代码
func add(int, int) int
// add函数的签名,实际上就是add的字面量类型
func (int, int) int
//匿名函数不能独立存在,常作为函数参数、返回值、或者赋值给某个变量
//匿名函数可以直接显式初始化
//匿名函数的类型也是函数字面量类型func (int,int) int
func (a,b int) int {
return a + b
}
//新定义函数类型ADD
//ADD底层类型是函数字面量类型func (int, int) int
type ADD func(int, int) int
//add和ADD的底层类型相同,并且add是字面量类型
//所以add可直接赋值给ADD类型的变量g
var g ADD = add
func main() {
f := func(a, b int) int {
return a + b
}
g(1, 2)
f(1, 2)
//f和add的函数签名相同
fmt.Printf("%T\n", f)
fmt.Printf("%T\n", add)
}
函数类型有如下意义:
(1)函数也是一种类型,可以在函数字面量类型的基础上定义一种命名函数类型
(2)有名函数和匿名函数的函数签名与命名函数类型的底层类型相同,它们之间可以进行类型转换
(3)可以为有名函数类型添加方法,可以方便地为一个函数增加“拦截”或“过滤”等额外功能,这提供了一种装饰设计模式
(4)为有名函数类型添加方法,使其与接口打通关系,使用接口的地方可以传递函数类型的变量。
至此,我们基本了解了Go原因的类型系统,下一部分我们将学习Go语言的接口。