结构体
结构体(struct)是一种自定义数据类型,它允许你将多个不同或相同类型的变量组合成一个单一的类型,类似于其他编程语言中的"类"。
结构体是 Go 语言中实现面向对象编程特性的一种方式,结构体但Go语言是面向过程的,没有类和继承的概念。
1、定义结构体
一个结构体由一组字段(field)组成,每个字段都有一个名称和类型。可以通过type
关键字来定义一个新的结构体类型。结构体的字段可以是任何类型,包括基本类型、其他结构体、切片、哈希表等。
go
type Product struct {
name string
price float64
}
2、创建结构体实例
可以使用两种方式来创建结构体实例:直接声明或使用 new
函数。
go
func Test1(t *testing.T) {
product1A := Product{Name: "苹果"} // 使用字面量创建结构体,不指定字段名,Go会自动分配默认值
product1B := Product{Name: "苹果", Price: 3.98}
product1C := Product{"苹果", 3.98} // 省略字段名
product1D := &Product{"苹果", 3.98} // 获取指针类型
fmt.Println(product1A)
fmt.Println(product1B)
fmt.Println(product1C)
fmt.Println(product1D)
fmt.Println("=========")
product2A := new(Product) // new创建空结构体,返回指针类型
product2A.Name = "手机"
product2A.Price = 4999.99
product2B := *new(Product) // 解引
product2B.Name = "手机"
product2B.Price = 4999.99
fmt.Println(product2A)
fmt.Println(product2B)
}
输出:
{苹果 0}
{苹果 3.98}
{苹果 3.98}
&{苹果 3.98}
=========
&{手机 4999.99}
{手机 4999.99}
3、嵌套与匿名字段
Golang支持嵌套其他结构体作为字段,并且允许匿名嵌入,这类似于继承的一种实现方式。如果结构体字段的类型名称前没有字段名,那么该字段就是匿名字段。
go
type NewProduct struct {
Product
Brand string
}
func Test2(t *testing.T) {
product := NewProduct{
Product: Product{
Name: "手机",
Price: 4999.99,
},
Brand: "oppp",
}
fmt.Println(product)
fmt.Println(product.Name)
fmt.Println(product.Price)
fmt.Println(product.Brand)
}
输出:
{{手机 4999.99} oppp}
手机
4999.99
oppp
4、函数类型字段
结构体内部不能直接定义函数,但可以包含函数类型的字段。这意味着你可以在结构体中声明一个字段,其类型是一个函数签名。这个字段本质上是一个指向某种特定签名的"回调"或"处理器"。
go
// 定义一个结构体,其中包含一个函数类型的字段
type Student2 struct {
num int
name string
sayHello func() string // 函数类型字段,用于存储具有特定签名的函数
}
func Test(t *testing.T) {
p := Student2{
num: 1,
name: "小明",
sayHello: func() string { return "我的名字是?" }, // 将匿名函数赋值给sayHello字段
}
str := p.sayHello() // 调用存储在sayHello中的具体实现
fmt.Println("sayHello:", str)
}
理解与应用
- 灵活性:通过这种方式,你可以为不同实例提供不同功能,实现类似策略模式(Strategy Pattern)的效果。
- 使用场景:通常用于需要动态行为配置或回调机制的数据模型设计。
注意:这是一种灵活但独立于具体数据模型之外的方法配置方式。它无法直接引用或操作同一数据模型内的其他成员。
5、结构体的方法
函数类型字段本身不能直接引用结构体的其他字段,因为它们只是简单的函数指针,没有绑定到特定的结构体实例上。然而,你可以通过方法(method)来实现对结构体字段的访问。
在 Go 中,方法的定义与普通函数类似,但需要在 func
关键字和方法名之间指定接收者(receiver)。接收者可以是值接收者或指针接收者。
5.1、值接收者
当一个方法的接收者是一个值类型时,该方法会对调用它的结构体实例进行值复制。也就是说,方法内部对该结构体字段的任何修改都不会影响原始实例。
go
func Test1(t *testing.T) {
student := Student{name: "小明", num: 12}
student.sayHello()
fmt.Println(student.num)
}
type Student struct {
num int
name string
}
func (s Student) sayHello() {
fmt.Printf("大家好,我叫%s \n", s.name)
// 值接受者,不会对原数据进行修改
s.num = 99
}
特点:
- 方法调用时,会复制整个结构体。
- 不会修改原始数据。
- 适用于小型数据结构,因为复制开销较小。
输出:
大家好,我叫小明
12
5.2、指针接收者
当一个方法的接收者是指针类型时,该方法会直接操作传入对象的内存地址。因此,任何对字段进行修改的方法都会影响到原始实例。
go
func Test2(t *testing.T) {
student := Student{name: "小明", num: 12}
student.sayHello2()
fmt.Println(student.num)
}
func (s *Student) sayHello2() {
fmt.Printf("大家好,我叫%s \n", s.name)
// 会对原数据进行修改
s.num = 99
}
特点:
- 方法调用时,不会复制整个结构体,而是传递内存地址。
- 可以修改原始数据。
- 更高效地处理大型数据结构,因为避免了不必要的数据拷贝。
- 必须使用指针才能实现某些接口(如果接口要求的是指针)。
输出:
大家好,我叫小明
99
5.3、如何选择?
-
需要修改对象状态:如果你的方法需要改变对象本身,比如更新某个字段,那么应该使用指针接收者。
-
避免拷贝开销:对于包含大量数据或复杂嵌套的数据类型,使用指针可以避免每次调用都进行完整的数据拷贝,提高性能。
-
一致性考虑:为了保持一致性,即使某些不需要改变状态的方法也可能选择用指针,以便与其他需要改变状态的方法保持一致。在同一类型上混合使用值和指针作为接受器可能导致代码难以理解,因此通常建议统一采用一种方式。
-
简单只读操作: 如果你的函数只是读取而不更改对象,可以考虑用值接受器,但前提是这个struct比较小且简单,这样做能让代码更直观。
5.4、与普通函数的区别
这两种函数定义的主要区别在于方法接收者和普通函数的使用方式以及语义上的差异。
方法(Method)
go
func (s Student) sayHello() {
fmt.Printf("大家好,我叫%s \n", s.name)
// 值接受者,不会对原数据进行修改
s.num = 99
}
- 接收者 :
(s Student)
是一个值接收者,这意味着当调用这个方法时,会对Student
实例进行值复制。 - 调用方式 :这种形式是将
sayHello()
作为类型Student
的方法,可以通过该类型的实例来调用 - 语义上:将函数与特定的数据结构绑定,表示该操作与数据结构紧密相关,通常用于实现接口或增强面向对象风格。
普通函数
go
func sayHello3(s Student){
fmt.Printf("大家好,我叫%s \n", s.name)
}
- 参数传递:这里的参数传递也是按值传递,即对参数进行复制。
- 调用方式:这是一个独立的函数,与任何类型无关。你需要显式地传入一个Student实例来调用它
- 语义上:表示这是一个独立于数据结构之外的功能,可以应用于任何符合参数要求的数据,而不必是某个特定类型的方法。
使用
-
如果你希望某个功能紧密绑定到某个数据类型,并且可能会用到接口实现,那么选择方法。
-
如果功能相对独立,不需要与具体的数据结构强关联,或者可能适用于多种不同的数据结构,那么选择普通函数。
-
在设计API时,如果希望提供一种更自然、更面向对象风格的接口,则可以考虑使用方法。否则,对于工具类、辅助类逻辑,普通函数可能更加合适。
6、结构体的比较
在Go语言中,结构体的比较是一个重要的特性。结构体可以通过比较运算符进行相等性测试,但需要满足一定条件。
可比较的条件
-
可比较字段:只有当结构体中的所有字段都是可比较类型时,整个结构体才是可比较的。基本数据类型(如整数、浮点数、布尔值、字符串)和指针都是可直接进行相等性测试的。
-
不可直接比较字段 :如果一个结构体包含切片(slice)、映射(map)、函数(function)或其他不支持直接相等运算符的数据类型,那么这个结构体不能使用
==
或!=
进行整体上的相等性测试。
可直接比较
go
type Point struct {
X, Y int
}
func main() {
p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
p3 := Point{X: 3, Y: 4}
fmt.Println(p1 == p2) // 输出:true,因为所有字段都相同
fmt.Println(p1 == p3) // 输出:false,因为字段不同
}
不可直接比较
go
type Data struct {
Numbers []int // 切片是不可以用==来做整体对比的
}
type Student struct {
num int
name string
sayHello func() string
}
func Test2(t *testing.T) {
d1 := Data{Numbers: []int{1, 2}}
d2 := Data{Numbers: []int{1, 2}}
// 以下代码会导致编译错误:
// invalid operation: d1 == d2 (struct containing []int cannot be compared)
//fmt.Println(d1 == d2)
fmt.Println(d1)
fmt.Println(d2)
var s1 = Student{1, "小明", func() string { return "x" }}
var s2 = Student{1, "小明", func() string { return "x" }}
// 以下代码会导致编译错误:
// invalid operation: s1 == s2 (struct containing func() string cannot be compared)
//fmt.Println(s1 == s2)
fmt.Println(s1)
fmt.Println(s2)
}
自定义Equal方法
对于包含不可比元素的数据类型,你可以通过自定义方法来实现自己的"相等"逻辑。例如,可以为切片逐个元素地进行对比:
go
// Equal 方法用于判断两个 Student 是否"相等"
func (s1 Student) Equal(s2 Student) bool {
if s1.num != s2.num {
return false
}
if s1.name != s2.name {
return false
}
return true
}
func Test3(t *testing.T) {
var s1 = Student{1, "小明", func() string { return "x" }}
var s2 = Student{1, "小明", func() string { return "x" }}
println(s1.Equal(s2)) // 输出true
}
注意事项
-
指针与零值:即使某些字段是指针,只要它们所指向的数据本身是可比的,并且没有出现nil解引用的问题,它们也是可以参与整体对比操作。
-
性能考虑:对于大型数据集或复杂嵌套的数据,手动实现对比可能会影响性能,因此在设计时应考虑到这一点。
思考
1、结构体声明时,可以指定默认值吗
结构体声明时不能直接指定默认值。结构体的字段在实例化时会自动初始化为其类型的零值。
不过,我们可以通过构造函数模式来实现类似于默认值的效果。
go
type InitStruct struct {
a string
b int
}
/*
定义一个构造函数,为结构体赋初始默认值
*/
func DefaultInit() InitStruct {
return InitStruct{a: "默认值", b: 99}
}
func Test3(t *testing.T) {
initStruct := DefaultInit()
fmt.Println(initStruct)
}