1. 类型简介
1.1 命名类型和非命名类型
1.1.1 命名类型
可以通过标识符来标识的类型,称为命名类型。
Go 语言的基本类型中有 20 个预声明简单类型都是命名类型,除此之外,用户自定义类型也是命名类型。
1.1.2 非命名类型
一个类型由预声明类型、关键字和操作符组合而成,这个类型称为未命名类型,未命名类型又称为类型字面量。
Go 语言的基本类型中的符合类型:数组(array)、切片(slice)、字典(map)、通道(channel)、指针(pointer)、函数字面量(function)、结构(struct)和接口(interface)都属于类型字面量,也都是未命名类型。
所以 *int
、[]int
、[2]int
、map[k] v
都是未命名类型。
package main
import "fmt"
type Person struct{
name string
age int
}
func main(){
// 使用 struct 字面量声明的是未命名类型
a := struct {
name string
age int
}{name: "王曌", age: 21}
fmt.Printf("a type = %T, value = %v\n", a, a)
b := Person{name: "wangzhao", age: 21}
fmt.Printf("b type = %T, value = %v\n", b, b)
}
1.3 小结
- 未命名类型和类型字面量是等价的,我们通常所说的 Go 语言基本类型中的符合类型就是类型字面量,所以未命名类型、类型字面量和 Go 语言基本类型中的符合类型三者是等价的。
- 通常所说的 Go 语言基本类型中的简单类型中的 20 个预声明类型,他们都属于命名类型。
- 预声明类型是命名类型的一种,另一类命名类型是自定义类型。
1.2 底层类型
所有 “类型” 都有一个
underlying type
(底层类型)。底层类型的规则如下:
- 预声明类型和类型字面量的底层类型是其自身。
- 自定义类型
type newType oldType
中newType
的底层类型是逐层向下递归查找,制导oldType
是预声明类型或类型字面量。
type T1 string
type T2 T1
type T3 []string
type T4 T3
type T5 []T1
type T6 T5
根据底层类型的规则,可以很容易判断出,T1、T2 的底层类型是 string,T3、T4 的底层类型是 []string,T5、T6的底层类型是 []T1。这里 T6、T5 与 T3、T4 的底层类型是不一样的,一个是 []T1,另一个是 []string。
1.3 类型相同和类型赋值
1.3.1 类型相同
Go 是强类型语言,编译器在编译时会进行严格的类型检验。两个命名类型是否相同,参考如下:
- 两个命名类型相同的条件是两个类型声明的语句完全相同
- 命名类型和未命名类型永远不相同
- 两个未命名类型相同的条件是它们的类型声明字面量的结构相同,并且内部元素的类型相同
- 通过类型别名声明的两个类型相同
1.3.2 类型赋值
不同类型的变量之间一般是不能直接相互赋值的,除非满足一定的条件。
类型为 T1 的变量 a 可以赋值给类型为 T2 的变量 b,称为类型 T1 可以赋值给类型 T2.
var a T1
var b T2 = a
a 可以赋值给变量 b 必须满足如下条件中的一个:
- T1 和 T2 的类型相同
- T1 和 T2 具有相同的底层类型,并且 T1 和 T2 里面至少有一个未命名类型
- T2 是接口类型,T1 是具体类型,T1 实现了 T2 的所有方法
- T1 和 T2 都是通道类型,它们拥有相同的元素类型,并且 T1 和 T2 中至少有一个是未命名类型
- a 是预声明标识符
nil
,T2 是pointer
、function
、slice
、map
、channel
、interface
类型中的一个 - a 是一个字面常量值,可以用来表示类型 T 的值
package main
import "fmt"
type Map map[string]string
func (m Map) Print(){ // 类型 Map 的方法
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 _, key := range s{
fmt.Println(key)
}
}
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
// Map 实现了 Print(),诉讼一其可以赋值给接口类型变量
var i interface{
Print()
} = ma
s1 := []int{1, 2, 3}
var s2 slice
s2 = s1
}
1.4 强制类型转换
在进行类型赋值时,如果不满足自动转换的条件时,则必须进行强制类型转换。任意两个不相干的类型如果进行强制类型转换,则必须符合一定的规则。强制类型的语法格式:
var a T = (T)(b)
,使用括号将类型和要转换的变量或表达式的值括起来。
非常量类型的变量 x 可以强制转化并传递给类型 T,需要满足如下任一条件:
- x 可以直接赋值给 T 类型变量
- x 的类型和 T 具有相同的底层类型
package main
import "fmt"
type Map map[string]string
func (m Map) Print(){ // 类型 Map 的方法
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 _, key := range s{
fmt.Println(key)
}
}
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 虽然有相同的底层类型,但是两者中没有一个字面量类型,不能直接赋值
// 可以强制进行类型转化
// var im iMap = ma
var im iMap = (iMap)(ma)
}
- x 的类型和 T 都是未命名的指针类型,并且指针指向的类型具有相同的底层类型。
- x 的类型和 T 都是整性,或者都是浮点型
- x 的类型和 T 都是复数类型
- x 式整数值或
[]byte
类型的值,T 是string
类型 - x 是一个字符串,T 是
[]byte
或[]rune
s := "hello,世界!"
var a []byte
a = []byte(s)
var b string
b = string(a)
var c []rune
c = []rune(s)
fmt.Printf("%T\n", a) // []uint8
fmt.Printf("%T\n", b) // string
fmt.Printf("%T\n", c) // []uint32
数值类型和string
类型之间的相互转换可能造成值部分丢失;其他的转化仅是类型的转化,不会造成值的改变。string
和数字之间的转换可以使用标准库 strconv
。
2. 类型方法
2.1 自定义类型
用户自定义类型使用关键字
type
,其语法格式是type newType oldType
,oldType
可以是自定义类型、预声明类型未命名类型中的任意一种。
2.1.1 自定义 struct 类型
package main
// 使用 type 自定义的结构类型属于命名类型
type xxx struct{
Field1 type1
Field2 type2
...
}
type errorString struct{
s string
}
// 结构字面量属于未命名类型
struct {
Field1 type1
Field2 type2
...
}
// struct{} 是非命名类型空结构
var s = struct {
}{}
2.1.2 struct 初始化
type Person struct{
name string
age int
}
- 按字段顺序进行初始化
// 有三种写法
a := Person{"wangzhao", 21}
b := Person{
"wangzhao",
21,
}
c := Person{
"wangzhao",
21}
- 指定字段名进行初始化
a := Person{name: "wangzhao", age: 21}
b := Person{
name: "wangzhao",
age: 21,
}
c := Person{
name: "wangzhao",
age: 21}
注意:如果 }
独占一行,则最后一个字段的后面一定要带上逗号。
- 使用
new
内置函数创建,字段默认初始化为对于类型的零值,返回值是指向结构的指针。
p := new(Person)
fmt.Printf("p type = %T, value = %v\n", p, p)
- 一次初始化一个字段
p := Person{}
p.name = "wangzhao"
p.age = 21
- 使用构造函数进行初始化
// 自定义构造函数
func NewPerson(name string, age int) *Person{
return &Person{name: name, age: age}
}
func main(){
var p *Person = NewPerson("wangzhao", 21)
fmt.Println(p)
}
2.1.3 结构字段的特点
结构的字段可以是任意的类型,结构字段的类型名必须唯一。结构支持内嵌自身的指针,这也是实现树形和链表等复杂数据结构的基础。
// 标准库 container/list
type Element struct{
// 指向自身类型的指针
list *List
Value interface{}
}
2.1.4 匿名字段
在定义
struct
过程中,如果字段只给出字段类型,没有给出字段名,则称这样的字段为“匿名字段”。
被匿名嵌入的字段必须是命名类型或命名类型的指针,类型字面量不能作为匿名字段使用。匿名字段的字段名默认就是类型名,如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。一个结构体里面不能同时存在某一类型及其指针类型的匿名字段,原因是二者的字段名相等。如果嵌入的字段来自其他包,则需要加上包名,并且必须是其他包可以到处的类型。
// 标准库 os/type.go 内的一个匿名的指针字段
type File struct{
*file
}
2.1.5 自定义接口类型
接口字面量是非命名类型。但自定义接口类型同样使用
type
关键字声明。
// interface{} 是接口字面量类型标识,所以 i 是非命名类型变量
// Reader 是自定义接口类型,属于命名类型
type Reader interface{
Read(p []byte)(n int, err error)
}
2.2 方法
在Java中,并没有对函数和方法进行区分,因为一切皆对象。而在 Go 中,函数和方法并不是完全相同的概念。Golang 中的方法是作用在指定的数据类型上,即,和指定的数据类型绑定,因此自定义类型,都可以有方法,而不仅仅是方法。
在某些情况下,我们需要定义方法。比如 Person 结构体,除了有一些字段外,Person 结构体还需要一些行为,可以说话、跑步等。这时候要用方法才能完成。
为命名类型定义方法的语法格式如下:
// 类型方法接收者是值类型
func (t TypeName) MethodName(ParamList)(ReturnList){
//method body
}
// 类型方法接收者是指针类型
func (t *TypeName) MethodName(ParamList)(ReturnList){
//method body
}
说明:
- t 是接收者,可以自由指定名称。
- TypeName 为命名类型的类型名
- MethodName 为方法名
- ParamList 形参列表
- ReturnList 返回值列表
Go 语言的类型方法本质上就是一个函数,没有使用隐式的指针。可以将类型的方法改写为常规的函数。
// 类型方法接收者是值类型
func MethodName(t TypeName, otherParamList)(ReturnList){
//method body
}
// 类型方法接收者是指针类型
func MethodName(t *TypeName, otherParamList)(ReturnList){
//method body
}
示例:
类型方法特点如下:
- 可以为命名类型增加方法(除了接口),非命名类型不能自定义方法
比如不能为[]int
类型增加方法,因为[]int
是非命名类型。命名接口类型本身就是一个方法的签名集合,所以不能为其增加具体的方法实现。 - 为类型增加方法有一个限制,就是方法的定义必须和类型的定义在同一个包中。
不能再为int
,bool
等预声明类型增加方法,因为它们是命名类型,但它们是 Go 语言内置的预声明类型,作用域是全局的,为这些类型新增的方法是在某个包中,与规则冲突,编译器拒绝增加方法。 - 方法的命名空间的可见性和变量一样,大写开头的方法可以在包外被访问,否则只能在包内可见。
- 使用
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.Println("c = ", c)
fmt.Println("d = ", d)
}
3. 方法调用
3.1 一般调用
类型方法的一般调用方式:
TypeInstanceName.MethodName(ParamList)
- TypeInstanceName:类型实例名或指向实例的指针变量名
- MethodName:类型方法名
- ParamList:方法实参
示例:
package main
import "fmt"
type T struct {
a int
}
func (t T) Get() int {
return t.a
}
func (t *T)Set(i int) {
// 注意,这里的 t 是一个指针,其标准访问字段的方式为:
// (*t).a = i
// 但是由于编译器底层进行了优化,所以
// (*t).a 等价 t.a
t.a = i
}
func main() {
var t = &T{}
t.Set(2)
// 注意,当前 t 变量为一个指针,但是类型方法的接收者是一个值类型,
// 所以标准调用方式如下:
// (*t).Get
// 但是由于编译器底层进行了优化,所以
// (*t).Get 等价 t.Get
// 由于 . 的优先级高于 *,所以不能写成 *t.Get()
value := t.Get()
fmt.Println(value)
}
3.2 方法值
变量
x
的静态类型是T
,M
是类型T
的一个方法,x.M
被称为方法值。x.M
是一个函数类型变量,可以赋值给其他变量,并向普通的函数名一样使用。
f := x.M
f(args...)
方法值其实就是一个带闭包的函数变量,其底层实现原理和带有闭包的匿名函数类型,接收值被隐式地绑定到方法值地闭包环境中。后续调用不需要再显式地传递接收者。
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{}
// 方法值
f := t.Set
// 方法值调用
f(2)
t.Print() //0xc0000140a0, &{2}, 2
// 方法值调用
f(3)
t.Print() //0xc0000140a0, &{3}, 3
}
3.3 方法表达式
方法表达式相当于提供一种语法将类型方法调用显式地转换为函数调用,接收者(receiver)必须显式地传递进去。
type T struct {
a int
}
// 方法接受者为 T
func (t T) Get() int {
return t.a
}
// 方法接受者为 *T
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
被称为方法表达式,方法表达式可以函数函数名,只不过这个函数地首个参数是接收者地实例或指针。T.Get
的函数签名是func(t T)int
,(*T).Set
的函数签名是func(t *T, i int)
。注意,这里的 T.Get
不能写成(*T).Get
,(*T).Set
也不能写成T.Set
,方法表达式在编译器中不会做自动转换。
// 如下方法表达式调用都是等价的
t := T{a: 1}
// 普通方法调用
t.Get()
// 方法表达式调用
(T).Get(t)
// 方法表达式调用
f1 := T.Get
f1(t)
f2 := (T).Get
f2(t)
// 如下方法表达式的调用是等价的
(*T).Set(&t, 2)
f3 := (*T).Set
f3(&t, 2)
3.4 方法集
无论方法的接收者是值类型还是指针类型,实参传递和函数的一样,都是值拷贝。如果接收者是值类型,则传递的是值的副本;如果接收者是指针类型,则传递的指针的副本。
package main
import "fmt"
type Int int
func (a Int) Max(b Int) Int{
if a > b{
return a
}
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() // 内部被编译器转换为 c.Print()
a.Set(20) // 内部被编译器转换为 (&a).Set(20)
a.Print()
(&a).Set(30)
a.Print()
}
接收者是 Int
类型的方法集合:
func (i Int)Print()
func (a Int)Max(b Int) Int
接收者是 *Int
类型的方法集合:
func (i *Int)Set(a Int)
为了简化描述,将接收者为值类型T
的方法的集合记录为S
,将接收者为指针类型*T
的方法的集合统称为*S
。类型的方法集总结如下:
T
类型的方法集是S
*T
类型的方法集是S
和*S
在直接使用类型实例调用类型方法时,无论值类型变量还是指针类型变量,都可以调用类型的所有方法,原因时编译器在编译期间能够识别出这种调用关系,做了自动转换。比如 a.Set()
使用值类型实例调用指针接收者方法,编译器会自动将其转换为(&a).Set()
,(&a).Print()
使用指针类型实例调用值类型接收者方法,编译器自动将其转化为a.Print()
。
3.5 值调用和表达式调用的方法集
方法的调用中,编译器会进行自动转化。在以下两种情况下编译器是否会进行方法的自动转化。
- 通过类型字面量显式地进行值调用和表达式调用,可以看到在这种情况下编译器不会做自动转换,会进行严格地方法集检查。
package main
type Data struct {
}
func (Data) TestValue(){
}
func (*Data) TestPointer(){
}
func main() {
// 这种字面量显式调用,无论值调用,还是表达式调用
// 编译器都不会进行方法集地自动转化,编译器会严格检验方法集
// *Data 的方法集是 TestPointer 和 TestValue
// Data 的方法集是 TestValue
(*Data)(&struct{}{}).TestPointer() // 显式调用
(*Data)(&struct{}{}).TestValue() // 显式调用
(Data)(struct{}{}).TestValue() // 方法值
Data.TestValue(struct{}{}) // 方法表达式
// 如下调用因为方法集不匹配而失败
// Data.TestPointer(struct{}{}) type Data has no method TestPointer
// (Data)(struct{}{}).TestPointer() cannot call pointer method on Data(struct {} literal)
}
- 通过类型字面量进行值调用和表达式调用,在这种情况下,使用值调用方式调用时会进行自动转化,使用表达式调用方式调用时编译器不会进行转化,会进行严格的方法集检查。
package main
type Data struct {
}
func (Data) TestValue(){
}
func (*Data) TestPointer(){
}
func main() {
// 声明一个类型变量 a
var a Data = struct{}{}
// 表达式调用编译器不会进行自动转化
Data.TestValue(a)
// Data.TestValue(&a)
(*Data).TestPointer(&a)
// (*Data).TestPointer(a)
// 值调用编译器会进行自动转化
a.TestValue()
(&a).TestValue()
a.TestPointer()
(&a).TestPointer()
}
4. 组合和方法集
4.1 组合
使用
type
定义的新类型不会继承原有类型的方法,但是命名结构类型可以嵌套其他的命名类型的字段,外层的结构类型时可以调用嵌入字段类型的方法,这种调用即可以是显式的调用,也可以是隐式的调用。这就是 Go 的“继承”,准确的说这就是 Go 的“组合”。因为 Go 语言没有继承的语义,结构和字段之间是 “has a” 的关系,而不是 “is a”的关系;没有父子的概念,仅仅是整体和局部的概念,所以称这种嵌套的结构和字段的关系为组合。
我个人更倾向于将此称为继承,因为组合的话可以认为结构体里面包含其他类型的字段,且该字段具有指定名称,如下:
type X struct {
a int
}
type Y struct {
x X
b int
}
上面的代码可以视作是结构 Y 中组合了一个类型为 X 的 x 变量。这里暂以李文塔老师书中的内容为准吧。
struct
类型中的字段称为“内嵌字段”。
4.1.1 内嵌字段的初始化和访问
struct
的字段访问使用点操作符“.”,struct
的字段可以嵌套很多层,只要内嵌的字段是唯一的即可,不需要使用全路径进行访问。
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X
b int
}
type Z struct {
Y
c int
}
func main() {
x := X{a: 1}
y := Y{
X: x,
b: 2,
}
z := Z{
Y: y,
c: 3,
}
// z.a, z.Y.a, z.Y.X.a 三者是等价的,z.a, z.Y.a 是 z.Y.X.a 的简写
fmt.Println(z.a, z.Y.a, z.Y.X.a) // 1 1 1
z = Z{}
z.a = 2
fmt.Println(z.a, z.Y.a, z.Y.X.a) // 2 2 2
}
在 struct
的多层嵌套中,不同嵌套层次可以有相同的字段,此时最好使用完全路径进行访问和初始化。在实际数据结构的定义中应该尽量避开相同的字段,避免出现歧义。
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X
a int
}
type Z struct {
Y
a int
}
func main() {
x := X{a: 1}
y := Y{
X: x,
a: 2,
}
z := Z{
Y: y,
a: 3,
}
// 此时 z.a, z.Y.a, z.Y.X.a 代表不同的字段
fmt.Println(z.a, z.Y.a, z.Y.X.a) // 3 2 1
z = Z{}
z.a = 4
z.Y.a = 5
z.Y.X.a = 6
fmt.Println(z.a, z.Y.a, z.Y.X.a) // 4 5 6
}
4.1.2 内嵌字段的方法调用
struct
类型方法调用也使用点操作符,不同嵌套层次的字段可以有相同的方法,外层变量调用内嵌字段的方法时也可以向嵌套字段的访问一样使用简化模式。如果外层字段和内存字段有相同的方法,则使用简化模式访问外层的方法会覆盖内存的方法。即在简写模式下,遵循最近访问原则。
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X
b int
}
type Z struct {
Y
c int
}
func (x X)Print(){
fmt.Println("In X, a = ", x.a)
}
func (x X)XPrint(){
fmt.Println("In X, a = ", x.a)
}
func (y Y)Print(){
fmt.Println("In Y, b = ", y.b)
}
func (z Z)Print(){
fmt.Println("In Z, c = ", z.c)
// 显式的完全路径调用内嵌字段的方法
z.Y.Print()
z.Y.X.Print()
}
func main() {
x := X{a: 1}
y := Y{
X: x,
b: 2,
}
z := Z{
Y: y,
c: 3,
}
// 根据就近原则,首先找到的是 Z 的 Print() 方法
z.Print()
// 根据最近原则,最后找到的是 X 的 XPrint() 方法
z.XPrint()
z.Y.XPrint()
}
4.2 组合的方法集
组合结构的方法集有如下规则:
- 若类型 S 包含匿名字段 T,则 S 的方法集包含 T 的方法集
- 若类型 S 包含匿名字段 *T,则 S 的方法集包含 T 和 *T 的方法集
package main
import "fmt"
type X struct {
a int
}
type Y struct {
X
}
type Z struct {
*X
}
func (x X)Get()int {
return x.a
}
func (x *X)Set(i int) {
x.a = i
}
func main() {
x := X{a: 1}
y := Y{
X:x,
}
fmt.Println(y.Get())
y.Set(2)
fmt.Println(y.Get())
// 为了不让编译器做自动转化,使用方法表达式调用方式
// Y 内嵌字段X,所以 type Y 的方法集是 Get, type *Y 的方法集是 Set、Get
(*Y).Set(&y, 3)
fmt.Println(y.Get())
// type Y的方法集没有 Set 方法,所以下一句编译不能通过
// Y.Set(y, 3)
z := Z{
X: &x,
}
// 按照嵌套字段的方法集规则
// Z 内嵌字段 *X,所以 type Z 和 type *Z 方法集都包含类型 X 定义的方法 Get 和 Set
// 为了不让编译器做自动转化,仍然使用方法表达式调用方法
Z.Set(z, 4)
fmt.Println(z.Get())
(*Z).Set(&z, 5)
fmt.Println(z.Get())
}
5. 函数类型
函数类型也分为两种,一种是函数字面量类型(未命名类型),另一种是函数命名类型。
5.1 函数字面量类型
函数字面量类型的语法表达式是 func(InputTypeList) OutputTypeList
,可以看出“有名函数”和“匿名函数”的类型都属于函数字面量类型。有名函数的定义相当于初始化一个函数字面量类型后将其赋值给一个函数名变量;“匿名函数”的定义也是直接初始化一个函数字面量类型,只是没有绑定到一个具体变量上。
5.2 函数命名类型
可以使用 type NewType OldType
语法定义一种新类型,这种类型都是命名类型,同理可以使用该方法定义一种新类型:函数命名类型,简称函数类型。例如:
type NewFuncType FuncLiteral
5.3 函数签名
"函数签名"就是“有名函数”或“匿名函数”的字面量类型。所以有名函数和匿名函数的函数签名可以相同,函数签名是函数的“字面量类信息”,不包括函数名。