1. 自定义类型格式
用户自定义类型使用关键字 type
,其语法格式是:
type newType oldType
oldType
可以是自定义类型、预声明类型、未命名类型中的任意一种。
newType
是新类型的标识符,与 oldType
具有相同的底层类型,并且都继承了底层类型的操作集合(这里的操作不是方法,比如底层类型是 map
,支持 range
迭代访问,则新类型也可以使用 range
迭代访问) 。除此之外, newType
和 oldType
是两个完全不同的类型, newType
不会继承 oldType
的方法。
无论 oldType
是什么类型,使用 type
声明的新类型都是一种命名类型,也就是说,自定义类型都是命名类型。
type INT int //INT 是一个使用预声明类型声明的自定义类型
type Map map[string]string //Map 是一个使用类型字面量声明的自定义类型
type myMap Map //myMap 是一个自定义类型Map 声明的自定义类型
// INT, Map 、myMap 都是命名类型
2. 自定义 struct 类型
struct
类型是 Go
语言自定义类型的普遍的形式,是 Go
语言类型扩展的基石,也是 Go
语言面向对象承载的基础。
前面章节将 struct
划为未命名类型,那时的 struct
是使用字面量来表示的,如果使用 type
语句声明,则这个新类型就是命名类型。例如:
// 使用 type 自定义的结构类型属于命名类型
type XXXName struct {
field1 type1
field2 type2
}
// errorString 是一个自定义结构类型,也是命名类型
type errorString struct {
s string
}
// 结构字面量属于未命名类型
struct {
field1 type1
field2 type2
}
// struct{} 是非命名类型空结构
var s = struct{}{}
2.1 struct 初始化
以 Person
结构为例来讲一下结构的初始化的方法。例如:
type Person struct {
name string
age int
}
- 按照字段顺序进行初始化
// 以下三种写法都可以
a := Person{"Tom", 25}
b := Person{
"Tom",
25}
c := Person{
"Tom",
25, // 必须加逗号
}
不推荐这种写法,一旦结构体增加字段,则需要重新修改初始化语句的顺序。
- 指定字段名进行初始化
推荐这种写法,结构体增加字段后,无需修改原有初始化语句的顺序。
a := Person{name:"Tom", age:25}
b := Person{
name:"Tom",
age:25}
b := Person{
name:"Tom",
age:25, // 加逗号
} // 初始化语句结尾的 } 独占一行,则最后一个字段的后面一定要带上逗号。
- 使用
new
创建内置函数,字段默认初始化为其类型的零值, 返回值是指向结构的指针。例如:
p := new(Person)
// 此时 name 为 "", age 为 0
这种方法不常用,一般使用 struct
都不会将所有字段初始化为零值。
- 一次初始化一个字段
p := Person{}
p.name = "Tom"
p.age = 25
这种方法不常用,这是一种结构化的编程思维,没有封装,违背了 struct
本身抽象封装的理念。
- 使用构造函数进行初始化
这是推荐的一种方法,当结构发生变化时,构造函数可以屏蔽细节。
type Cat struct {
Color string
Name string
}
func NewCatByName(name string) *Cat {
return &Cat{
Name: name, // 忽略 Color 字段
}
}
func NewCatByColor(color string) *Cat {
return &Cat{
Color: color, // 忽略 Name 字段
}
}
- 带有父子关系结构体初始化
type Cat struct {
Color string
Name string
}
type BlackCat struct {
Cat
}
// 基类
func NewCat(name string) *Cat {
return &Cat{
Name: name, // 忽略 Color 字段
}
}
// 子类
func NewBlackCat(color string) *BlackCat {
cat := &BlackCat{} // 实例化 BlackCat 结构,此时 Cat 也同时被实例化。
// 填充 BlackCat 中 嵌入的 Cat 颜色属性。 BlackCat 没有任何成员,所有的成员都来自于 Cat
cat.Color = color
return cat
}
2.2 结构字段的特点
结构的字段可以是任意的类型,基本类型、接口类型、指针类型、函数类型都可以作为 struct
的字段。结构字段的类型名必须唯一, struct
字段类型可以是普通类型,也可以是指针。另外,结构支持内嵌自身的指针,这也是实现树形和链表等复杂数据结构的基础。例如:
// 标准库container/list
type Element struct {
// 指向自身类型的指针
next, prev *Element
list *List
Value interface{}
}
2.3 匿名字段
结构体允许其成员字段在声明时没有宇段名而只有类型,这种形式的字段被称为类型内嵌或匿名字段。
在定义 struct
的过程中,如果字段只给出字段类型,没有给出宇段名, 则称这样的字段为“匿名字段”。被匿名嵌入的字段必须是命名类型或命名类型的指针,类型字面量不能作为匿名字段使用。
type Data struct {
int
float32
bool
}
func main() {
d := &Data{
int: 10,
float32: 3.14,
bool: false,
}
fmt.Printf("%+v", d)
}
类型内嵌其实仍然拥有自己的字段名,只是字段名就是其类型本身而己,,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
匿名字段的字段名默认就是类型名,如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。但一个结构体里面不能同时存在某一类型及其指针类型的匿名字段, 原因是二者的字段名相等。如果嵌入的字段来自其他包,则需要加上包名,并且必须是其他包可以导出的类型。
2.4 结构体内嵌
结构体实例化后,如果匿名的字段类型为结构体,那么可以直接访问匿名结构体里的所有成员,这种方式被称为结构体内嵌。
2.4.1 结构体内嵌示例
// 基础颜色
type BasicColor struct {
R, G, B float32
}
// 完整颜色
type Color struct {
// 将基本颜色作为成员
Basic BasicColor
// 透明度
Alpha float32
}
func main() {
c := new(Color)
// 设置基本颜色值
c.Basic.B = 1
c.Basic.G = 1
c.Basic.R = 0
c.Alpha = 0.5
fmt.Printf("%+v", c)
}
需要通过 Basic
结构才能设置 R
、 G
、 B
分量,虽然合理但是写法很复杂。使用 Go
语言的结构体内嵌写法重新调整代码如下 :
type BasicColor struct {
R, G, B float32
}
type Color struct {
// 将 BasicColor 结构体嵌入到 Color 结构体中,
// BasicColor 没有宇段名而只有类型,这种写法就叫做结构体内嵌 。
BasicColor
Alpha float32
}
func main() {
c := new(Color)
// 可以直接对 Color 的 R、 G 、 B 成员进行设置,编译器通过 Color 的
// 定义知道 R、 G 、 B 成员来自 BasicColor 内嵌的结构体。
c.B = 1
c.G = 1
c.R = 0
c.Alpha = 0.5
fmt.Printf("%+v", c)
}
2.4.2 结构体内嵌特性
- 内嵌的结构体可以直接访问其成员变量
嵌入结构体的成员,可以通过外部结构体的实例直接访问。如果结构体有多层嵌入结构体,结构体实例访问任意一级的嵌入结构体成员时都只用给出字段名,而无须像传统结构体字段一样,通过一层层的结构体字段访问到最终的宇段。
例如,ins.a.b.c
的访问可以简化为ins.c
。
- 内嵌结构体的字段名是它的类型名
内嵌结构体字段仍然可以使用详细的宇段进行一层层访问,内嵌结构体的字段名就是它的类型名,代码如下:
c := new(Color)
c.BasicColor.B = 1
c.BasicColor.G = 1
c.BasicColor.R = 0
一个结构体只能嵌入一个同类型的成员,无须担心结构体重名和错误赋值的情况,编译器在发现可能的赋值歧义时会报错。
3. 自定义接口类型
接口字面量是非命名类型,但自定义接口类型是命名类型。自定义接口类型同样使用 type
关键字声明。示例如下:
// interface{} 是接口字面量类型标识, 所以 i 是非命名类型交量
var i interface{}
// Reader 是自定义接口类型,属于命名类型
type Reader interface {
Read (p []byte) (n int , err error )
}
4. 为什么要使用类型定义
类型定义可以在原类型的基础上创造出新的类型,有些场合下可以使代码更加简洁,如下边示例代码:
package main
import (
"fmt"
)
// 定义一个接收一个字符串类型参数的函数类型
type handle func(str string)
// exec函数,接收handle类型的参数
func exec(f handle) {
f("hello")
}
func main() {
// 定义一个函数类型变量,这个函数接收一个字符串类型的参数
var p = func(str string) {
fmt.Println("first", str)
}
exec(p)
// 匿名函数作为参数直接传递给exec函数
exec(func(str string) {
fmt.Println("second", str)
})
}
输出结果:
first hello
second hello
上边的示例是类型定义的一种简单应用场合,如果不使用类型定义,那么想要实现上边示例中的功能,应该怎么书写这段代码呢?
// exec函数,接收handle类型的参数
func exec(f func(str string)) {
f("hello")
}
exec
函数中的参数类型,需要替换成 func(str string)
了,咋一看去也不复杂,但是假如 exec
接收一个需要 5 个参数的函数变量呢?是不是感觉参数列表就会很长了。
func exec(f func(str string, str2 string, num int, money float64, flag bool)) {
f("hello")
}
从上边的代码可以发现,exec
函数的参数列表可读性变差了。下边再来看看使用类型定义是怎么实现这个功能:
package main
import (
"fmt"
)
// 定义一个需要五个参数的函数类型
type handle func(str string, str2 string, num int, money float64, flag bool)
// exec函数,接收handle类型的参数
func exec(f handle) {
f("hello", "world", 10, 11.23, true)
}
func demo(str string, str2 string, num int, money float64, flag bool) {
fmt.Println(str, str2, num, money, flag)
}
func main() {
exec(demo)
}