06 复合类型类型 - 结构体的使用和注意事项
- 结构体(
struct)是一种复合数据类型,用于将不同类型的数据组合成一个整体。 - 它可以用来定义复杂的数据模型,并且在编写面向对象风格的代码时非常有用。
- 通过结构体,可以更好地表示具有多个属性的实体,并将这些属性封装在一起。
定义结构体
在 Go 中,使用 type 关键字来定义一个结构体类型。结构体的定义通常放在全局作用域。
package main
import "fmt"
// 定义一个结构体类型
type Person struct {
Name string
Age int
City string
}
func main() {
// 创建结构体实例
var p1 Person // 使用零值初始化
p1.Name = "Alice"
p1.Age = 30
p1.City = "New York"
fmt.Println(p1) // 输出:{Alice 30 New York}
// 使用结构体字面量创建
p2 := Person{Name: "Bob", Age: 25, City: "San Francisco"}
fmt.Println(p2) // 输出:{Bob 25 San Francisco}
}- 结构体字段(例如
Name,Age,City)可以是任何数据类型,包括基本类型(如string、int)、数组、切片、指针、甚至是其他结构体类型。 - 字段名必须以大写字母开头才能被其他包访问(导出字段),如果是小写字母开头,则只能在当前包中使用(未导出字段)。
结构体的实例化
type Person struct {
Name string
Age int
City string
}当声明一个结构体变量时,如果没有显式初始化,则所有字段都会被设置为该字段类型的默认值。
govar p1 Person // 所有字段都被设置为默认值("",0,nil 等) fmt.Println(p1) // 输出:{ 0 }使用结构体字面量来初始化。
gop2 := Person{"Charlie", 22, "Seattle"} fmt.Println(p2) // 输出:{Charlie 22 Seattle}或者使用命名字段的方式(推荐),这样初始化时可以按任意顺序指定字段。
gop3 := Person{Name: "David", City: "Los Angeles"} fmt.Println(p3) // 输出:{David 0 Los Angeles} (未指定的字段使用默认值)使用
new关键字可以创建结构体的指针实例。(很少使用,不推荐!)gop4 := new(Person) p4.Name = "Eve" p4.Age = 28 p4.City = "Boston" fmt.Println(*p4) // 输出:{Eve 28 Boston}使用
&操作符来创建结构体的指针。gop5 := &Person{Name: "Frank", Age: 35, City: "Chicago"} fmt.Println(*p5) // 输出:{Frank 35 Chicago}
结构体字段的访问与修改
可以使用点操作符(.)来访问和修改结构体字段。
p := Person{Name: "Grace", Age: 40, City: "New York"}
fmt.Println(p.Name) // 输出:Grace
// 修改字段值
p.Age = 41
fmt.Println(p.Age) // 输出:41对于结构体指针,也可以直接使用点操作符访问字段。
p := &Person{Name: "Hank", Age: 45, City: "Los Angeles"}
fmt.Println(p.Age) // 输出:45(自动解引用)嵌套结构体
- 结构体可以包含其他结构体作为字段,从而形成嵌套结构。
- 使用嵌套结构体可以更好地组织和表示复杂的数据关系。
type Address struct {
Street string
City string
Zip string
}
type User struct {
Name string
Age int
Contact Address
}
func main() {
user := User{
Name: "John",
Age: 30,
Contact: Address{
Street: "123 Main St",
City: "New York",
Zip: "10001",
},
}
fmt.Println(user) // 输出:{John 30 {123 Main St New York 10001}}
fmt.Println(user.Contact) // 输出:{123 Main St New York 10001}
fmt.Println(user.Contact.City) // 输出:New York
}匿名字段与结构体嵌入
Go 中允许在结构体中使用匿名字段(嵌入其他结构体类型),这可以被认为是 Go 中的一种简化的继承方式。
type Person struct {
Name string
Age int
}
type Employee struct {
Person // 匿名字段,嵌入了 Person 结构体
ID int
Company string
}
func main() {
emp := Employee{
Person: Person{Name: "Mike", Age: 29},
ID: 1001,
Company: "Tech Corp",
}
fmt.Println(emp) // 输出:{{Mike 29} 1001 Tech Corp}
// 直接访问嵌入字段
fmt.Println(emp.Name) // 输出:Mike
fmt.Println(emp.Age) // 输出:29
}Employee嵌入了Person结构体,可以直接访问Person的字段(例如Name和Age),就像它们是Employee自身的字段一样。
方法与结构体
结构体可以绑定方法,使其具备类似面向对象编程中的“类”功能。方法的接收者(receiver)可以是结构体的值类型或指针类型。
type Rectangle struct {
Width float64
Height float64
}
// 绑定值类型的接收者
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 绑定指针类型的接收者
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
fmt.Println("Area:", rect.Area()) // 输出:Area: 50
rect.Scale(2)
fmt.Println("Scaled Area:", rect.Area()) // 输出:Scaled Area: 200
}- 值接收者:方法接收结构体的副本,不会影响原结构体。
- 指针接收者:方法接收结构体的指针,可以修改原结构体的内容。
比较结构体
在 Go 中,结构体只能进行相等性比较(== 或 !=),前提是所有字段都是可比较的类型。如果两个结构体的字段值完全相同,则它们被认为是相等的。
type Point struct {
X, Y int
}
p1 := Point{X: 1, Y: 2}
p2 := Point{X: 1, Y: 2}
fmt.Println(p1 == p2) // 输出:true结构体标签
Go 结构体可以使用标签(tag)为字段添加元数据,常用于 JSON 序列化、数据库映射等场景。
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
import "encoding/json"
u := User{Name: "Tom", Age: 20}
jsonData, _ := json.Marshal(u)
fmt.Println(string(jsonData)) // 输出:{"name":"Tom","age":20}结构体的默认值
结构体的零值是其字段类型的默认值。即使没有初始化字段,结构体依然可以正常使用。
type MyStruct struct {
Name string
Age int
Valid bool
}
var s MyStruct
fmt.Println(s) // 输出:{ 0 false}结构体指针
- 结构体是值类型。这意味着当你将一个结构体赋值给另一个变量时,实际上是创建了这个结构体的一个副本,而不是引用原始结构体。
- 为了避免这种副本操作,可以使用指针来引用结构体,从而节省内存,并且可以直接修改原结构体的数据。
&操作符:获取变量的指针(地址)。*操作符:通过指针访问变量的值。
package main
import "fmt"
type Person struct {
Name string
Age int
}
func main() {
// 使用值类型创建结构体
p1 := Person{Name: "Alice", Age: 25}
p2 := p1 // p2 是 p1 的副本
p2.Age = 30 // 修改 p2 的 Age 字段,不会影响 p1
fmt.Println(p1) // 输出:{Alice 25}
fmt.Println(p2) // 输出:{Alice 30}
// 使用结构体指针
p3 := &p1 // p3 是 p1 的指针
p3.Age = 35 // 修改 p3 的 Age 字段,会影响 p1
fmt.Println(p1) // 输出:{Alice 35}
fmt.Println(*p3) // 输出:{Alice 35}
}使用 & 创建结构体指针
可以使用 & 操作符来创建一个结构体的指针。这样创建的指针变量可以直接操作结构体的字段。
type Rectangle struct {
Width float64
Height float64
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
rectPointer := &rect // 使用 & 操作符获取结构体指针
fmt.Println(rectPointer) // 输出:&{10 5}
// 修改指针指向的结构体字段
rectPointer.Width = 20
fmt.Println(rect) // 输出:{20 5}
}使用 new 创建结构体指针
Go 提供了 new 关键字用于创建结构体指针实例。new 函数分配内存并返回结构体的指针,但不会进行初始化。
type Circle struct {
Radius float64
}
func main() {
c := new(Circle) // 返回 *Circle 类型的指针
fmt.Println(c) // 输出:&{0} (字段未初始化)
// 通过指针修改字段
c.Radius = 10
fmt.Println(*c) // 输出:{10}
}**`new` 和 `&` 的区别**
new仅分配内存并返回指针,所有字段都被设置为零值。- 使用
&取地址时,可以对结构体字段进行初始化。
结构体指针的字段访问
当你有一个结构体指针时,可以通过 . 操作符直接访问和修改它的字段,而不需要显式地使用 * 来解引用(Go 编译器会自动处理解引用操作)。
package main
import "fmt"
type Book struct {
Title string
Author string
}
func main() {
book := Book{Title: "Go Programming", Author: "Alice"}
bookPtr := &book // 获取结构体的指针
// 使用指针访问和修改字段
// bookPtr.Title 实际上等价于 (*bookPtr).Title
// Go 会自动解引用结构体指针,简化代码书写
bookPtr.Title = "Advanced Go"
fmt.Println(book) // 输出:{Advanced Go Alice}
}结构体指针作为函数参数
使用结构体指针作为函数参数,可以避免结构体的副本拷贝,并且可以在函数中修改原结构体的数据。
package main
import "fmt"
type Employee struct {
Name string
Age int
}
// 值传递(不会修改原结构体)
func updateAgeByValue(e Employee) {
e.Age = 50
}
// 指针传递(会修改原结构体)
func updateAgeByPointer(e *Employee) {
e.Age = 50
}
func main() {
emp := Employee{Name: "John", Age: 30}
updateAgeByValue(emp)
fmt.Println(emp) // 输出:{John 30} (没有变化)
updateAgeByPointer(&emp)
fmt.Println(emp) // 输出:{John 50} (发生变化)
}- 在
updateAgeByValue函数中,传入的是结构体的副本,所以对e.Age的修改不会影响原结构体。 - 在
updateAgeByPointer函数中,传入的是结构体的指针,因此修改指针指向的字段,会影响原结构体。
结构体方法中的指针接收者与值接收者
在结构体方法中,可以选择使用值接收者或指针接收者。指针接收者允许修改结构体的内容,而值接收者则是结构体的副本,无法修改原始数据。
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
// 值接收者方法(不会修改原始数据)
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
// 指针接收者方法(可以修改原始数据)
func (r *Rectangle) Scale(factor float64) {
r.Width *= factor
r.Height *= factor
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
// 使用值接收者调用方法(不会改变原结构体)
fmt.Println("Area:", rect.Area()) // 输出:Area: 50
// 使用指针接收者调用方法(改变了原结构体)
rect.Scale(2)
fmt.Println("Scaled Rectangle:", rect) // 输出:Scaled Rectangle: {20 10}
}- 当一个方法使用值接收者时,它只能访问和操作结构体的副本。
- 当一个方法使用指针接收者时,它可以直接修改结构体本身。
指针接收者的最佳实践
一般来说,使用指针接收者有以下场景:
- 结构体很大,传递指针比值拷贝更高效。
- 需要修改原结构体的内容。
- 保持一致性:如果一个方法需要指针接收者,那么所有方法都应该使用指针接收者。