本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
Go语言中的类型既有预定义的,也允许用户自定义。正如在Java中我们可以自定义类一样,在Go中我们可以通过自定义类型来创造新的数据类型。
复合字面量可以为结构、数组、切片或映射类型构造值,字面量的底层类型必须是以上类型,该规则受语法的强制约束,除非类型以类型名称给出。元素和键的类型必须可分配给字面量类型的相应字段、元素和键的类型;没有额外的转换。字面量的键将解释为结构字面量的字段名,数组或切片的索引,或映射的键。指定具有相同字段名称或常量键值的多个元素是错误的。
一 类型声明
类型声明将新的标识符(类型名称)绑定到类型。类型声明有两种形式:别名声明和类型定义。
1.1 别名声明
语法:
type 别名(标识符) = 类型
别名只是一个新的标识符,但并未创建新的类型。作为该类型的别名,自然与该类型是一样的。
例如,以下示例我们新定义了一个类型别名,其中Time
与[]int
具有相同的类型:
package main
import "fmt"
func main() {
var a Time
fmt.Printf("%T", a)
}
type Time = []int // Time是[]int的一个类型别名
可以同时声明多个类型别名:
type (
Time = []int // Time 和 []int 是相同的类型
Color = []string // Color 和 []string 是相同的类型
Point = struct{ x, y int } // Point 和 struct{ x, y int } 是相同的类型
A = Point // A, Point 和 struct{ x, y int } 是相同的类型
)
1.2 类型定义
与别名声明不同,类型定义会创建一个新的,不同的类型,其具有与给定类型相同的底层类型和操作。
语法:
type 新类型名称(标识符) 已定义类型|struct{}
注意与别名声明的区别,新类型标识符与创建它的类型之间没有
=
符号。
从语法中可以看出,有两种方法可以自定义类型:基于已有的类型,使用struct
关键字。
新类型称为已定义类型。它与任何其他类型不同,包括创建它的类型。例如type A int
定义了一个新的类型A
,其底层就是一个int
类型,但是A
与int
仍然是不同的两个类型。
示例:
type (
Point struct{ x, y float64 } // Point 和 struct{ x, y float64 } 是不同的类型
polar Point // polar 和 Point 也是不同的类型
)
// 新的结构类型
type TreeNode struct {
left, right *TreeNode
value *Comparable
}
// 新的接口类型
type Block interface {
BlockSize() int
Encrypt(src, dst []byte)
Decrypt(src, dst []byte)
}
1.2.1 借助已有类型定义
我们可以基于已定义的类型定义新的类型,以下示例中我们基于uint类型创建的新的计数器类型Counter
:
package main
import "fmt"
func main() {
var c Counter
var i uint = 8
// c = i // 错误: cannot use i (type uint) as type Counter in assignment
c = 9 // OK.
fmt.Printf("%T, %T\n", c, i) // main.Counter, uint
fmt.Printf("%v, %v\n", c, i) // main.Counter, uint
}
type Counter uint // Counter 是一个新的类型
虽然Counter
底层就是由uint
构成,但它们却是不同的类型,从打印出的结果也能看出。
另外,由于Go语言中对已定义的类型之间不能做隐式转换,故上述示例中的第9行代码如果去掉注释的话不能编译通过。
1.2.2 使用struct关键字定义
使用struct
关键字定义的类型称为结构类型。结构(struct
)就是一组字段(field
)的集合。字段名称可以显式指定(标识符列表)或隐式指定(内嵌字段)。在结构中,非空白字段名称必须是唯一的。
// 一个空的结构类型
struct {}
// 7个字段的结构类型
struct {
x, y int
u float32
// u string // 重复的字段 u
_ float32 // padding
_ int // 空白标识符可以重复
A *[]int
F func()
}
注意类型定义与类型的区别。
示例:
package main
import "fmt"
// Point用于表示xy坐标轴上的点
type Point struct {
X int // 字段
Y int // 字段
}
1.2.3 初始化
定义好之后,我们就可以像其他内置类型一样来使用它了:
var p Point // 定义一个点p
fmt.Println(p) // {0 0}
声明了一个变量后,这个变量首先会被初始化为该类型的零值,对于struct
类型的零值,是其各个字段的零值。对于我们定义的上述变量p
,其两个字段会首先初始化为0。
有2种方法可以为一个struct
变量赋值:结构字面量、点号(.
):
结构字面量
形如TypeName{key1: value1, key2: value2, ...}
:
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
p0 := Point{Y: 5} // 指定键名时,可以只给指定的字段赋值,其他缺省为零值:{0 5}
p1 := Point{X: 2, Y: 3} // 分别指定每个字段的值:{2 3}
p2 := Point{Y: 4, X: 3} // 指定字段名的时候,字段顺序无所谓:{3 4}
p3 := Point{12, 13} // 忽略字段名:{12 13}
p4 := Point{13, 12} // 不指定字段名时,顺序很重要,要严格按照字段声明的顺序给出对应的值:{13 12}
p5 := Point{} // 零值,等价于 var p5 Point
fmt.Println(p0, p1, p2, p3, p4, p5)
}
需要注意的是,使用结构字面量初始化时,如果每个字段单独一行,则每一行结尾必须要有一个逗号:
p := Point{
5,
12,
}
对于结构字面量,适用于以下规则:
- 键必须是在结构体中声明的字段。
- 不包含键名时,元素顺序要严格按照结构中字段的声明顺序的顺序。
- 如果任何一个元素有键,其他元素都要有。
- 在给出键名的前提下,无需按顺序赋值,也无需给全部字段赋值,缺省字段将使用其零值初始化。
- 字面量也可以忽略全部元素的赋值,这样的字面量相当于该类型的零值。
- 不能为属于不同包的结构的非导出字段指定元素。
获取复合字面量的地址会生成一个指向该字面量值初始化的唯一变量的指针。
var pointer *Point = &Point{X: 2, Y: 3}
点号
除了使用上述字面量的方式直接初始化结构类型的字段值,还可以使用点号来访问或赋值:
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
var p Point
fmt.Println(p) //{0 0}
p.X = 2
p.Y = 3
fmt.Println(p) //{2 3}
}
1.2.4 字段标记
字段声明时,可以包含一个可选的字符串字面量标记,该标记将作为该行声明的所有字段的一个属性。空字符串标记相当于没有标记。标记通过反射接口可见。并且是类型标识的一部分,其他情况下可以忽略。
type T struct {
x, y float64 "" // 空标记相当于没有标记
name string "任何字符串都可以作为标记"
_ [4]byte "ceci n'est pas un champ de structure(法语)"
}
// 表示TimeStamp协议缓冲区的结构。标记定义了协议缓冲区字段的序号;
// 它们遵循反射包概述的惯例
struct {
microsec uint64 `protobuf:"1"`
serverIP6 uint64 `protobuf:"2"`
}
1.3 结构指针
结构字段可以通过结构指针来访问。
如果我们有一个指向结构体的指针 p,那么可以通过 (*p).X 来访问其字段 X。不过这么写太麻烦,所以Go语言也允许我们使用隐式间接引用,直接写 p.X 就可以:
package main
import "fmt"
type Point struct {
X int
Y int
}
func main() {
v := Point{2, 3}
var p = &v
fmt.Println(v) // {2 3}
(*p).Y = 5
fmt.Println(v) // {2 5}
p.Y = 7
fmt.Println(v) // {2 7}
}
二 类型标识
两个类型要么相同要么不同。
已定义的类型总是与任何其他类型不同。除此之外,如果两个类型的底层类型字面量在结构上相同,则它们是相同的;换句话说,它们有相同的字面结构,并且相关组件也是一样的类型,具体细节如下:
- 如果两个数组类型具有相同的元素类型和数组长度,则它们相同。
- 如果两个切片类型具有相同的元素类型,则它们相同。
- 如果两个结构类型具有相同的字段序列,并且相关字段具有相同的名称、类型和标记,则它们相同。但未导出的字段名如果来自不同的包,则也是不同的。
- 如果两个指针类型具有相同的基础类型,则它们相同。
- 如果两个函数类型具有相同的参数及结果值,并且参数及结果值的类型也是相同的,并且两个函数要么全是可变参数,要么都不是,则它们相同。参数及结果名称可以不一样。
- 如果两个接口类型具有相同的方法集,且方法集的名称和函数类型一样,则它们一样。但未导出的方法名如果来自不同的包,则也是不同的。方法的出现顺序无所谓。
- 如果两个映射类型具有相同的
key
和value
类型,则它们相同。 - 如果两个信道类型具有相同的元素类型和方向,则它们相同。
给定以下声明:
type (
A0 = []string
A1 = A0
A2 = struct{ a, b int }
A3 = int
A4 = func(A3, float64) *A0
A5 = func(x int, _ float64) *[]string
)
type (
B0 A0
B1 []string
B2 struct{ a, b int }
B3 struct{ a, c int }
B4 func(int, float64) *B0
B5 func(x int, y float64) *A1
)
type C0 = B0
其中这些声明是相同的:
A0, A1, 和 []string
A2 和 struct{ a, b int }
A3 和 int
A4, func(int, float64) *[]string, 和 A5
B0 和 C0
[]int 和 []int
struct{ a, b *T5 } 和 struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), 和 A5
B0
和B1
不同是因为它们是由不同的类型定义创建的新类型;func(int, float64) *B0
和func(x int, y float64) *[]string
不同是因为B0
和[]string
不同。
参考:
https://golang.org/ref/spec#Type_declarations
https://golang.org/ref/spec#Type_identity
https://golang.org/ref/spec#Struct_types
https://golang.org/ref/spec#Composite_literals