- 结构体
- 类型别名和自定义类型
- 类型别名
- 结构体
- 创建结构体实例
- 访问结构体字段
- 修改结构体字段
- 嵌套结构体
- 结构体方法
- 结构体内存布局
- [题 关于 range 循环的陷阱](#题 关于 range 循环的陷阱)
- 构造函数
- [方法 和 接收者](#方法 和 接收者)
- 定义方法
- 结构体的匿名字段
- 匿名字段的优势
- [冲突 如果嵌入了多个具有相同字段名的结构体,访问时需要选择明确的路径:](#冲突 如果嵌入了多个具有相同字段名的结构体,访问时需要选择明确的路径:)
- 结构体的"继承"
- 结构体与JSON序列化
- 结构体标签(Tag)
- 注意事项
结构体
在 Go 语言中,结构体(struct)是一种复合数据类型,它允许将相关的数据组合在一起,形成一个实体。
通过结构体,开发者能够定义复杂的数据结构,这对于组织和管理数据非常有帮助。
类型别名和自定义类型
自定义类型
在Go语言中有一些基本的数据类型,
如string
、整型
、浮点型
、布尔
等数据类型,
Go语言中可以使用type
关键字来定义自定义类型。
自定义类型是定义了一个全新的类型。
//将MyInt定义为int类型
type MyInt int
类型别名
type byte = uint8
type rune = int32
c
package main
import "fmt"
func main() {
//类型定义
type NewInt int
//类型别名
type MyInt = int
var a NewInt
var b MyInt
fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
fmt.Printf("type of b:%T\n", b) //type of b:int
}
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
结构体
c
type 类型名 struct {
字段名 字段类型
字段名 字段类型
...
}
- 类型名:标识自定义结构体的名称,在同一个包内不能重复。
- 字段名:表示结构体字段名。结构体中的字段名必须唯一。
- 字段类型:表示结构体字段的具体类型。
c
package main
import "fmt"
// 定义一个结构体 Person
type Person struct {
Name string // 姓名
Age int // 年龄
}
公有(Exported):如果结构体的字段名(或者任何标识符,如变量、函数、方法等)的首字母是大写,那么这个字段是公有的,可以被其他包(package)访问和使用。
私有(Unexported):如果结构体的字段名的首字母是小写,那么这个字段是私有的,仅能在定义该字段的包内访问,外部包无法访问。
c
package main
import (
"fmt"
)
// 定义一个名为 Address 的结构体
type Address struct {
city string // 私有字段,首字母小写
Country string // 公有字段,首字母大写
}
// 定义一个名为 Person 的结构体,嵌套 Address
type Person struct {
Name string
Age int
Address // 嵌套的结构体
}
func main() {
// 创建一个 Person 实例
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
city: "Beijing", // 这里可以直接访问私有字段
Country: "China",
},
}
// 在包内,我们可以访问公有字段
fmt.Println("Person Name:", p.Name)
fmt.Println("Person Age:", p.Age)
fmt.Println("Person Country:", p.Address.Country) // 可以访问公有字段
// 下面这行代码将无法编译,因为 `city` 字段是私有的
// fmt.Println("Person City:", p.Address.city) // 会报错
}
创建结构体实例
可以通过直接赋值或使用结构字面量来创建结构体的实例。
c
func main() {
// 直接赋值创建结构体实例
person1 := Person{Name: "Alice", Age: 30}
// 只指定部分字段(未指定的字段使用零值)
person2 := Person{Age: 25} // Name 默认为空字符串
var p2 = new(person)// 指针方式创建结构体实例
fmt.Println(person1) // 输出: {Alice 30}
fmt.Println(person2) // 输出: { 25}
}
访问结构体字段
可以通过点操作符(.)来访问结构体的字段。
c
func main() {
person := Person{Name: "Bob", Age: 40}
fmt.Println("姓名:", person.Name) // 输出: 姓名: Bob
fmt.Println("年龄:", person.Age) // 输出: 年龄: 40
}
修改结构体字段
结构体字段是可以被修改的,使用点操作符来修改字段的值。
c
func main() {
person := Person{Name: "Charlie", Age: 35}
person.Age = 36 // 修改年龄
fmt.Println("修改后的年龄:", person.Age) // 输出: 修改后的年龄: 36
}
嵌套结构体
结构体可以包含其他结构体作为字段,从而形成复杂的数据结构。
c
type Address struct {
City string
ZipCode string
}
type Person struct {
Name string
Age int
Address Address // 嵌套结构体
}
func main() {
person := Person{
Name: "David",
Age: 28,
Address: Address{
City: "New York",
ZipCode: "10001",
},
}
fmt.Println("地址:", person.Address.City, person.Address.ZipCode) // 输出: 地址: New York 10001
}
结构体方法
结构体可以定义方法,方法是与结构体类型关联的函数。使用接收者(receiver)来定义方法。
c
func (p Person) Greet() {
fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}
func main() {
person := Person{Name: "Eve", Age: 22}
person.Greet() // 输出: Hello, my name is Eve and I am 22 years old.
}
结构体内存布局
c
package main
import (
"fmt"
"unsafe"
)
对齐规则
结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(#pragma pack(n))或当前成员变量类型的长度(unsafe.Sizeof),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n))或结构体的所有成员变量类型中的最大长度,取最大数的最小整数倍作为对齐值
结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n))超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的
type Part1 struct {
a bool // 1字节
b int32 // 4字节
c int8 // 1字节
d int64 // 8字节
e byte // 1字节
}
func main() {
part1 := Part1{}
//打印结构体的大小和对齐值
fmt.Printf("part1 size: %d, align: %d\n", unsafe.Sizeof(part1), unsafe.Alignof(part1))
}
part1 size: 32, align: 8
空结构体
空结构体是不占用空间的。
c
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
题 关于 range 循环的陷阱
c
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name: "bob", age: 18},
{name: "alice", age: 23},
{name: "tom", age: 9000},
}
//打印切片地址
fmt.Printf("切片地址1: %p\n", &stus[0])
fmt.Printf("切片地址2: %p\n", &stus[1])
fmt.Printf("切片地址3: %p\n", &stus[2])
//遍历切片,将每个元素的地址存入map
for k, _ := range stus {
m[stus[k].name] = &stus[k]
}
// or 安全方式遍历切片,将每个元素的地址存入map
for i := 0; i < len(stus); i++ {
m[stus[i].name] = &stus[i]
}
//遍历的时候每次都从里面复制一份,给stu ,取出stu'的地址没用,他的地址从来不变
for _, stu := range stus {
fmt.Printf("当前学生 %s 的地址: %p\n", stu.name, &stu) // 打印地址
m[stu.name] = &stu //此处获取的都是切片的地址
//! Go 语言中的 for range 循环的底层实现通过使用迭代器来遍历集合。在处理切片、数组或映射时,编译器会生成代码来管理这些集合的遍历,而每个元素的值在取用时并不是引用,而是值的副本。
//
//! 但是在循环内部的,循环变量的地址是一直保持不变的。每次迭代时,循环变量会被新的值覆盖,导致最后的所有指针指向同一个地址。
//! &stu 是取 stu 变量的地址。由于 stu 是在 for 循环中定义的局部变量,每次循环都会用新值来覆盖 stu。
//! 这意味着 m[stu.name] 会存储指向 stu 的地址,而不是存储一个新副本的地址。结果是,每次循环中 stu 的值都会被更新,导致映射 m 中的所有键都指向同一个地址,也就是最后一次迭代结束后 stu 的值。
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
构造函数
在 Go 语言中,构造函数并不是一个语言特性,但我们通常通过自定义函数来实现结构体的构造。
构造函数的主要功能是创建并初始化结构体的实例。
因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。
c
package main
import "fmt"
// 定义一个结构体
type Person struct {
Name string
Age int
}
// 构造函数,返回一个 Person 的实例
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
func main() {
// 使用构造函数创建一个 Person 的实例
person := NewPerson("Alice", 30)
// 输出实例的内容
fmt.Println("Name:", person.Name)
fmt.Println("Age:", person.Age)
}
方法 和 接收者
方法是与特定类型(通常是结构体)关联的函数,而接收者是方法的一个特定参数,表示调用该方法的实例。
通过接收者,方法可以访问和修改实例的数据。
定义方法
方法的定义形式如下:
c
func (receiver ReceiverType) MethodName(parameters) returnType {
// 方法的实现
}
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
函数体
}
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是`self`、`this`之类的命名。例如,`Person`类型的接收者变量应该命名为 `p`,`Connector`类型的接收者变量应该命名为`c`等。
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
- 方法名、参数列表、返回参数:具体格式与函数定义相同。
c
package main
import "fmt"
// 定义一个结构体
type Circle struct {
Radius float64
}
// 计算圆的面积的方法,接收者是 Circle
func (c Circle) Area() float64 {
return 3.14 * c.Radius * c.Radius // 使用接收者 c 计算面积
}
// 修改圆的半径的方法,接收者是 Circle 的指针
func (c *Circle) SetRadius(radius float64) {
c.Radius = radius // 修改接收者 c 的 Radius 字段
}
func main() {
// 创建一个 Circle 实例
circle := Circle{Radius: 5}
// 调用方法计算面积
fmt.Println("Area of circle:", circle.Area())
// 修改圆的半径
circle.SetRadius(10)
fmt.Println("New area of circle:", circle.Area())
}
什么时候应该使用指针类型接收者
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int
类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。
c
package main
import "fmt"
// 定义一个新的自定义类型 Int
type Int int
// 方法:计算该类型值的平方
func (i Int) Square() Int {
return i * i // 返回平方值
}
// 方法:判断该类型值是否为偶数
func (i Int) IsEven() bool {
return i%2 == 0 // 判断是否为偶数
}
// 方法:计算该类型值的三倍
func (i Int) Triple() Int {
return i * 3 // 返回三倍值
}
func main() {
var myInt Int = 4 // 创建自定义类型的实例
// 调用自定义类型的方法
fmt.Println("Value:", myInt)
fmt.Println("Square:", myInt.Square())
fmt.Println("Is Even:", myInt.IsEven())
fmt.Println("Triple:", myInt.Triple())
}
结构体的匿名字段
c
package main
import "fmt"
// 定义一个结构体 Address
type Address struct {
City string
Country string
}
// 定义一个结构体 Person,使用匿名字段嵌入 Address
type Person struct {
Name string
Age int
Address // 匿名字段
}
func main() {
// 创建一个 Person 的实例,并初始化数据
p := Person{
Name: "Alice",
Age: 30,
Address: Address{
City: "Beijing",
Country: "China",
},
}
// 访问 Person 的字段
fmt.Println("Name:", p.Name)
fmt.Println("Age:", p.Age)
fmt.Println("City:", p.City) // 直接访问匿名字段中的 City
fmt.Println("Country:", p.Country) // 直接访问匿名字段中的 Country
}
匿名字段的优势
简化结构体访问:在访问嵌入结构体的字段时,不需要指定完整的字段路径,简化了代码。
重用性:可以通过匿名字段重用结构体,避免字段名称的重复。
注意事项
如果多个嵌入结构体有相同的字段名,则需要使用完整的路径进行访问,以避免歧义。
匿名字段也可以是接口类型,这使得可以实现多态。
冲突 如果嵌入了多个具有相同字段名的结构体,访问时需要选择明确的路径:
c
type Address struct {
City string
}
type Location struct {
City string
}
type Person struct {
Name string
Address // 匿名字段
Location // 匿名字段
}
func main() {
// 创建 Person 实例
p := Person{
Name: "Alice",
Address: Address{
City: "Beijing",
},
Location: Location{
City: "Shanghai",
},
}
// 访问字段
fmt.Println("Name:", p.Name)
fmt.Println("Address City:", p.Address.City) // 访问 Address 的 City
fmt.Println("Location City:", p.Location.City) // 访问 Location 的 City
}
结构体的"继承"
结构体没有传统意义上的继承(如在其他面向对象编程语言中那样),
但可以通过嵌套(匿名字段)来实现类似的功能。
这种方式允许一个结构体嵌入另一个结构体,从而获得其字段和方法的访问权。这种机制可以模拟某种形式的"继承"。
c
package main
import "fmt"
// 定义一个基础结构体 Animal
type Animal struct {
Name string
}
// 为 Animal 类型定义方法
func (a Animal) Speak() string {
return "Some sound"
}
// 定义一个结构体 Dog,嵌入 Animal 作为匿名字段
type Dog struct {
Animal // 嵌入 Animal,实现继承
Breed string
}
// 为 Dog 类型定义一个方法,覆盖基类方法
func (d Dog) Speak() string {
return "Woof!"
}
func main() {
// 创建一个 Dog 的实例
myDog := Dog{
Animal: Animal{Name: "Buddy"},
Breed: "Golden Retriever",
}
// 访问 Dog 的字段和方法
fmt.Println("Dog Name:", myDog.Name) // 从 Animal继承的字段
fmt.Println("Dog Breed:", myDog.Breed) // Dog 自身的字段
fmt.Println("Dog Speak:", myDog.Speak()) // Dog 自己的方法
}
代码讲解
定义基类结构体:
Animal 结构体包含一个字段 Name,和一个方法 Speak(),返回一个字符串表示动物的叫声。
继承结构体:
Dog 结构体嵌入 Animal,这样 Dog 就可以访问 Animal 的字段和方法。
Dog 结构体还包含自己的字段 Breed。
方法覆盖:
在 Dog 类型中定义了一个 Speak() 方法,从而覆盖了 Animal 的 Speak() 方法。这种方式可以让子类提供自己的实现,模拟多态行为。
创建实例:
在 main 函数中,创建了 Dog 的实例 myDog,并且通过访问字段和方法展示了继承的特性。
优势与特点
组合优于继承:Go 推荐使用组合(通过嵌套结构体)而非传统的类继承模型。通过组合,可以创建更灵活且可重用的代码结构。
多重嵌套:Go 允许多个匿名字段,这使得实现多重组合成为可能,但要注意字段名冲突的问题。
结构体与JSON序列化
在 Go 语言中,使用标准库 encoding/json 可以方便地将结构体与 JSON 数据进行序列化(编码)和反序列化(解码)。
序列化与反序列化基本概念
序列化(Serialization):将 Go 结构体编码为 JSON 格式的字符串。
反序列化(Deserialization):将 JSON 格式的字符串解码为 Go 结构体。
c
package main
import (
"encoding/json"
"fmt"
"log"
)
// 定义一个结构体,使用 `json` 标签指定 JSON 字段名称
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Address string `json:"address,omitempty"` // omitempty 表示如果为空则不在 JSON 中显示
}
func main() {
// 创建一个 Person 实例
person := Person{
Name: "Alice",
Age: 30,
Address: "123 Main St",
}
// 序列化结构体为 JSON
jsonData, err := json.Marshal(person)
if err != nil {
log.Fatal(err)
}
fmt.Println("JSON Output:", string(jsonData))
// 反序列化 JSON 为结构体
jsonInput := `{"name":"Bob","age":25,"address":"456 Elm St"}`
var newPerson Person
err = json.Unmarshal([]byte(jsonInput), &newPerson)
if err != nil {
log.Fatal(err)
}
// 输出反序列化后的结构体
fmt.Println("Deserialized Person:", newPerson)
}
代码讲解
定义结构体:定义了一个 Person 结构体,并使用 json 标签指定 JSON 中的字段名称。
json:"name" 表示在序列化和反序列化时,与 JSON 中的 name 字段对应。
omitempty 表示如果 Address 字段是空字符串,则在 JSON 中不显示该字段。
序列化:
使用 json.Marshal 函数将 Person 结构体实例 person 转换为 JSON 字符串。
错误处理确保在序列化过程中出现问题时能够得到反馈。
反序列化:
使用 json.Unmarshal 函数将 JSON 字符串 jsonInput 转换为 Person 结构体实例 newPerson。
将 JSON 字符串转换为字节切片,并传入 newPerson 的地址,以便填充结构体。
输出结果:
输出序列化后的 JSON 字符串和反序列化后的结构体字段值。
c
JSON Output: {"name":"Alice","age":30,"address":"123 Main St"}
Deserialized Person: {Bob 25 456 Elm St}
注意事项
字段可见性:只有导出的(公有的)结构体字段(首字母大写)才能被 JSON 序列化和反序列化。
json 标签:使用标签定制 JSON 字段名、忽略字段的序列化等,灵活满足不同需求。
时间类型:使用 time.Time 类型时,JSON 序列化和反序列化能够自动处理时间格式,但需要注意时区等问题。
结构体标签(Tag)
结构体标签(Tag)是 Go 语言的一种特性,允许开发者为结构体字段添加额外的信息。
这些信息通常在序列化和反序列化、数据验证等操作中使用。
标签被定义在结构体字段声明的反引号内,格式通常为 key:"value"。
使用结构体标签的场景
JSON 序列化和反序列化:指定 JSON 字段名。
数据库映射:在 ORM(对象关系映射)库中指定数据库列名。
数据验证:使用验证库进行字段值的约束和检查。
c
package main
import (
"encoding/json"
"fmt"
)
// 定义一个结构体,使用标签
type User struct {
ID int `json:"id"` // JSON 输出时的字段名为 "id"
Username string `json:"username"` // JSON 输出时的字段名为 "username"
Email string `json:"email,omitempty"` // 如果 Email 为空,则在 JSON 中省略该字段
Age int `json:"age" validate:"min=0"` // JSON 输出时的字段名为 "age",且添加验证标签
}
func main() {
// 创建一个 User 实例
user := User{
ID: 1,
Username: "Alice",
Email: "",
Age: 25,
}
// 序列化结构体为 JSON
jsonData, err := json.Marshal(user)
if err != nil {
fmt.Println("Error marshaling to JSON:", err)
return
}
// 输出 JSON 结果
fmt.Println("JSON Output:", string(jsonData))
// 反序列化 JSON 数据
jsonInput := `{"id":2,"username":"Bob","email":"bob@example.com","age":30}`
var newUser User
err = json.Unmarshal([]byte(jsonInput), &newUser)
if err != nil {
fmt.Println("Error unmarshaling JSON:", err)
return
}
// 输出反序列化后的结构体
fmt.Println("Deserialized User:", newUser)
}
代码讲解
定义结构体:
User 结构体包含多个字段,并在每个字段后添加了标签。
使用 json 标签指定在 JSON 中对应的字段名。例如,Username 字段在 JSON 中展示为 username。
omitempty 选项表示如果该字段为空,则在 JSON 输出中省略该字段。
validate 标签可用于第三方库进行自定义验证。
序列化:
json.Marshal 方法将 User 实例转换为 JSON 格式的字符串。
错误处理确保在序列化过程中出现问题时反馈。
反序列化:
json.Unmarshal 方法将 JSON 字符串转换为 User 实例。
使用 &newUser 传入指针,以便填充结构体。
注意事项
标签格式:标签的格式是一个关键值对,可以有多个键值对,使用空间分隔。
json:"name" validate:"required"
结构体字段可见性:标签只能应用于导出的字段(首字母大写),私有字段(首字母小写)无法被外部包访问。
第三方库:标签的具体语义和使用方法通常与具体实现的库相关,例如 JSON 序列化、ORM、数据验证库等