11 结构体
11 结构体
Go 没有类的概念,而是和 c 语言一样是结构体,可以用来表示一组相关的数据,相当于其他语言中的类,但更加灵活和简洁。
type
在了解结构体之前,先看看 Go 中的自定义类型和类型别名。
自定义类型
使用 type
关键字可以定义新的类型:
type 新类型 已有类型
看一个简单的例子:
type myInt int
var a myInt = 100
fmt.Printf("type: %T\n", a) // type: main.myInt
类型别名
类型别名本质上是给一个类型起一个新名字:
type 新名字 = 老名字
使用上也很简单:
type myInt = int
var b myInt = 100
fmt.Printf("type: %T\n", b) // type: int
值得注意的是,这种方式仅仅只是起了个新名字,但是并没有创建新的类型,前面我们见到过类似的类型别名有如下两个:
type byte = uint8
type rune = int32
结构体定义
使用 type
和 struct
关键字定义结构体:
type var_name struct {
字段名 类型
...
}
一个简单的例子:
type Person struct {
name string
age int
city string
}
// 相同类型的字段可以写在一行
type Person struct {
name, city string
age int
}
注意:起的变量名首字母大写表示公有,可以在所有包里使用,小写表示私有,只能在当前包进行使用。
结构体实例化
结构体实例化的方式比较多,可以自行选择:
// 声明后赋值
var p1 Person // 返回一个对象,或者 p1 := Person{}
p1.name = "张三"
// new
var p2 *Person := new(Person) // 返回指针,或者 p2 := &Person{}
p2.name = "李四" // Go 支持指针直接访问字段
// 键值对初始化
p3 := Person{
name: "赵六",
age: 25,
city: "上海",
}
// 指针方式
p4 := &Person{
name: "钱七",
age: 30,
}
// 值列表初始化,这种方式必须按字段声明顺序定义所有变量
p5 := Person{"周八", 28, "深圳"}
总之,我们可以定义一个指针或者纯对象,后续使用上并无区别,因为在 Go 简化了此操作: p.name
等价于 (*p).name
。
结构体方法
Go 不可以直接给一个结构体定义方法,需要在外部通过定义有接收者的函数来实现,接受者的概念类似于 this 这种东西。
func (接收者变量名 接收者类型) 函数名(函数参数) 返回值 {}
可以看到,与普通函数相比,多了一个 () 用于指定接收者,一般来说变量名为对应结构体的首字母小写,然后后面的类型根据读写情况需要区分是传入值还是传入指针。
理由也很简单,默认传参为拷贝,那么修改操作是不会生效的,必须传入指针才可以。
// 值接收者
func (p Person) getName() string {
return p.name
}
// 指针接收者
func (p *Person) setAge(age int) {
p.age = age
}
func main() {
p := Person{name: "Alice", age: 20}
fmt.Println(p.getName()) // Alice
p.setAge(25)
fmt.Println(p.age) // 25
}
给任意类型添加方法
对一个结构体可以通过这种方式来增加新方法,那对于其他的自定义类型呢,其实都可以通过这种方式来增加新方法。
type MyInt int
func (m MyInt) String() string {
return fmt.Sprintf("MyInt: %d", m)
}
func main() {
var n MyInt = 42
fmt.Println(n.String()) // MyInt: 42
}
但是请注意,只能为本包内的类型定义方法。
匿名字段
结构体可以包含匿名字段,字段名默认为类型名,因此这种方式一种类型只能写一个,否则就无法区分了:
type Person struct {
string // 姓名
int // 年龄
}
func main() {
p := Person{"Alice", 25}
fmt.Println(p.string, p.int) // Alice 25
}
嵌套结构体
普通嵌套
嵌套说白了就是把一个结构体当作另外一个结构体的成员,因此自然有 值嵌套和指针嵌套 的区别。
值嵌套时每个外层结构体都有自己独立的嵌套结构体的副本,修改一个不会影响其他;但是指针嵌套时多个外层结构体可以共享同一个嵌套结构体,修改会影响所有引用该指针的对象。
一般情况下推荐使用值嵌套。
type Address struct {
province string
city string
}
type User struct {
name string
address Address
}
func main() {
u := User{
name: "Bob",
address: Address{
province: "广东",
city: "深圳",
},
}
}
匿名嵌套
type User struct {
name string
Address // 匿名嵌套
}
func main() {
u := User{name: "Charlie"}
u.province = "北京" // 直接访问嵌套的匿名结构体字段
u.Address.city = "朝阳" // 或通过类型名访问
}
字段名冲突
上一个例子我们说了对象可以直接访问匿名结构体的成员,那如果我一个结构体里有多个匿名结构体,然后它们有一个同样的字段名,此时就理所当然无法区分了,必须老老实实写出全路径。
type Address struct {
createTime string
}
type Email struct {
createTime string
}
type User struct {
Address
Email
}
func main() {
u := User{}
u.Address.createTime = "2023"
u.Email.createTime = "2024"
// u.createTime = "error" // 编译错误:模糊的选择器
}
结构体继承
先说在前面,Go 的结构体没有原生的继承概念,但是如果不考虑访问限制的话,继承无非是使一个结构体获得其他结构体的成员和方法,而不用重复书写,那自然想到了刚刚介绍的匿名结构体嵌套,我们可以通过这种方式来模拟一个继承的行为:
type Animal struct {
name string
}
func (a Animal) run() {
fmt.Println("i can run")
}
type Dog struct {
age int32
Animal
}
func (d Dog) printInfo() {
fmt.Println(d.name, d.age)
d.run()
}
func main() {
dog := Dog{
age: 20,
Animal: Animal{
name: "dog",
},
}
dog.printInfo()
}
通过这种方式,我们实现了类似的继承效果,这也是 Go 的设计哲学: 组合优于继承。