2

struct 结构体【GO 基础】 - 橙子家

 11 months ago
source link: https://www.cnblogs.com/hnzhengfy/p/Go_struct.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

虽然 Go 语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念,但是可以通过结构体的内嵌,再配合接口,来实现面向对象,甚至具有更高的扩展性和灵活性。那么本文就将详细看下怎么使用结构体。

一、结构体的定义和实例化

Go 语言中的基础数据类型可以表示一些事物的基本属性,但是当想要表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了。

Go 语言提供了一种自定义数据类型可以封装多个基本数据类型,这种数据类型叫结构体 struct。通过 struct 可以封装自己所需的各种复杂类型。

1.1 什么叫做自定义类型和类型别名?

在详解结构体之前,需要先了解两个概念,到底什么叫做自定义类型和类型别名?

【自定义类型】

在 Go 语言中有一些基本的数据类型,如 string、整型、浮点型、布尔等数据类型,Go 语言中可以使用 type 关键字来定义自定义类型。

自定义类型是定义了一个全新的类型。我们可以基于内置的基本类型定义,也可以通过 struct 定义。

type MyInt int // 新定义一个类型 MyInt,以 int 类型为参照

通过 type 关键字的定义,MyInt 就是一种新的类型,它具有 int 的特性。

【类型别名】

类型别名是 Go1.9 版本添加的新功能。

类型别名规定:TypeAlias 只是 Type 的别名,本质上 TypeAlias 与 Type 是同一个类型。就像一个小孩子有自己的小名和户口本的大名,都是指的同一个人。

type TypeAlias = Type

例如长江的 rune 和 byte 就是类型别名,他们的定义如下:

type byte = uint8type rune = int32

【区别】

那么它们之前的区别是啥呢?下面分别定义一个变量看下它们的类型:

package main import "fmt" // 自定义类型type NewInt int // 类型别名type MyInt = int func main() { var a NewInt var b MyInt fmt.Printf("NewInt type of a : %T\n", a) fmt.Printf("MyInt type of b : %T\n", b)}

1868241-20231012115037545-1238334794.png

有输出结果可知,自定义类型的 type 将变成新的名字 NewInt;类型别名 MyInt,则仍属于基本的 int 类型。

1.2 结构体的定义

使用 type 和 struct 关键字来定义结构体。

type 类型名 struct { 字段名 字段类型 字段名 字段类型 …}// 类型名:标识自定义结构体的名称,在同一个包内不能重复// 字段名:表示结构体字段名,结构体中的字段名必须唯一// 字段类型:表示结构体字段的具体类型

举个例子,我们定义一个 Person(人)结构体:

type Person struct { name string city string age int8}// 类型相同的字段可以放到一起,如下:type Person struct { name, city string age int8}

这样我们就拥有了一个 Person 的自定义类型,它有 name、city、age 三个字段,分别表示姓名、城市和年龄。这样我们使用这个 person 结构体就能够很方便的在程序中表示和存储人信息了。 

语言内置的基础数据类型是用来描述一个值的,而结构体是用来描述一组值的。比如一个人有名字、年龄和居住城市等,本质上是一种聚合型的数据类型。

1.3 结构体的实例化

只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段

// 结构体实例化格式:var 结构体实例名 结构体类型

下面是一个 Person 的示例:

package main import "fmt" type Person struct { name string city string age int8} func main() { var person Person person.name = "中国" person.city = "北京" person.age = 18 fmt.Printf("person.name = %v\n", person.name) fmt.Printf("person %%v = %v\n", person) fmt.Printf("person %%#v = %#v\n", person)}

1868241-20231012135744441-1181874159.png

二、结构体的使用

2.1 匿名结构体

结构体还可以直接用于临时的或仅使用一次的场景中,不用声明结构体的名称,例如

package main import ( "fmt") func main() { var user struct { Name string Age int } user.Name = "张三" user.Age = 18 fmt.Printf("%#v\n", user)}

1868241-20231012142953384-1711577796.png

通过匿名结构体声明得来的实例 user。

2.2 指针类型的结构体

当通过使用 new 关键字对结构体进行实例化后,得到的是结构体的地址

另外,Go 语言中支持对结构体指针直接使用‘.’来访问结构体的成员。如下示例:

package main import ( "fmt") type Person struct { name string city string age int8} func main() { var p = new(Person) // 声明结构体指针 // p3 := &Person{} // 还可以通过 & 对结构体进行取地址操作,相当于对该结构体类型进行了一次 new 实例化操作 p.name = "测试" p.age = 18 p.city = "北京" fmt.Printf("%T\n", p) // 打印出类型 fmt.Printf("p = %#v\n", p)}

1868241-20231012144719402-2019982767.png

2.3 结构体的初始化

【声明即初始化】

此时结构体的值均为默认零值:

package main import ( "fmt") type Person struct { name string city string age int8} func main() { // 声明即初始化 var p = new(Person) fmt.Printf("p = %#v\n", p)}

1868241-20231012152257333-765623439.png

【使用键值对初始化】

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

// 使用键值对初始化p1 := Person{ name: "张三", city: "北京", age: 18,}fmt.Printf("p1 = %#v\n", p1)// 对结构体指针进行键值对初始化p2 := &Person{ name: "张三", city: "北京", age: 18,}fmt.Printf("p2 = %#v\n", p2)

1868241-20231012152355392-1481136011.png

【使用值的列表初始化】

初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值。
// 使用值的列表初始化p3 := &Person{ "张三", "北京", 18,}fmt.Printf("p3 = %#v\n", p3)

注意,值列表需要满足以下要求:

  • 必须初始化结构体的所有字段。
  • 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  • 该方式不能和键值初始化方式混用。

2.4 结构体的内存布局

package main import ( "fmt") type test struct { a int8 b int8 c int8 d int8} func main() { n := test{ 1, 2, 3, 4, } fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) fmt.Printf("n.d %p\n", &n.d)}

1868241-20231012153408815-297845078.png

2.5 结构体的构造函数

Go 语言的结构体没有构造函数,但可以自己实现。例如,下方的代码就实现了一个 Person 的构造函数。因为 struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

package main import ( "fmt") type Person struct { name string city string age int8} func newPerson(name, city string, age int8) *Person { return &Person{ name: name, city: city, age: age, }} func main() { p9 := newPerson("张三", "测试", 90) fmt.Printf("%#v\n", p9)}

1868241-20231012155234625-177330082.png

2.6 方法和接收者

Go 语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self。

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { 函数体}// 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是 self、this 之类的命名// 例如,Person 类型的接收者变量应该命名为 p,Connector 类型的接收者变量应该命名为 c 等// 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型// 方法名、参数列表、返回参数:具体格式与函数定义相同

下面是一个简单示例:

package main import ( "fmt") // Person 结构体type Person struct { name string age int8} // NewPerson 构造函数func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, }} // Dream Person 做梦的方法func (p Person) Dream() { fmt.Printf("%s 的梦想是学好 Go 语言!\n", p.name)} func main() { p1 := NewPerson("张三同学", 25) p1.Dream()}

1868241-20231012155919657-267786878.png

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型

【比较指针类型和值类型的接收者的区别】

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的

这种方式就十分接近于其他语言中面向对象中的 this 或者 self。 例如我们为 Person 添加一个 SetAge 方法,来修改实例变量的年龄。

当方法作用于值类型接收者时,Go 语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

package main import ( "fmt") // Person 结构体type Person struct { name string age int8} // NewPerson 构造函数func NewPerson(name string, age int8) *Person { return &Person{ name: name, age: age, }} // SetAge 设置p的年龄// 使用指针接收者func (p *Person) SetAge(newAge int8) { p.age = newAge} // SetAge2 设置p的年龄// 使用值接收者func (p Person) SetAge2(newAge int8) { p.age = newAge} func main() { p1 := NewPerson("张三同学", 25) fmt.Println("修改前的 age :", p1.age) p1.SetAge(18) fmt.Println("修改后的 age :", p1.age) p1.SetAge2(20) fmt.Println("修改后的 age :", p1.age)}

1868241-20231012160913604-892565488.png

使用指针类型的情况:

  • 需要修改接收者中的值。
  • 接收者是拷贝代价比较大的大对象。
  • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

【任意类型都可以添加方法】

比如下面示例代码,给自定义类型添加方法:

package main import ( "fmt") // 自定义一个类型 MyInt,参考 int 类型type MyInt int // 为 MyInt 添加一个 SayHello 的方法func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个 int。")} func main() { var m1 MyInt m1.SayHello() m1 = 100 fmt.Printf("%#v %T\n", m1, m1)}

1868241-20231012162504823-1466028410.png

2.7 结构体允许包含匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

package main import ( "fmt") // Person 结构体Person类型type Person struct { string int} func main() { p1 := Person{ "张三", 18, } fmt.Printf("%#v\n", p1) fmt.Println(p1.string, p1.int)}

1868241-20231012164123203-1511536640.png

匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

2.8 嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针。

package main import ( "fmt") // Address 地址结构体type Address struct { Province string City string} // User 用户结构体type User struct { Name string Gender string Address Address // 此处可以省略第二个 Address 以匿名类型方式} func main() { user1 := User{ Name: "张三", Gender: "女", Address: Address{ Province: "北京", City: "北京", }, } fmt.Printf("user1 = %#v\n", user1)}

1868241-20231012165214342-1970585599.png

当嵌套结构体内部存在相同的字段名时,为了避免歧义需要指定具体的内嵌结构体的字段,否则会提示异常。

2.9 结构体实现“继承”

如下示例代码,通过嵌套匿名结构体实现继承:
package main import ( "fmt") // Animal 动物type Animal struct { name string} func (a *Animal) move() { fmt.Printf("%s会动!\n", a.name)} // Dog 狗type Dog struct { Feet int8 *Animal // 通过嵌套匿名结构体实现继承} func (d *Dog) wang() { fmt.Printf("%s会汪汪汪~\n", d.name)} func main() { d1 := &Dog{ Feet: 4, Animal: &Animal{ // 注意嵌套的是结构体指针 name: "乐乐", }, } d1.wang() d1.move()}

1868241-20231012170745902-984040471.png

注意:结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

2.10 结构体与 JSON 序列化

JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,易于人阅读和编写,同时也易于机器解析和生成。

JSON 键值对是用来保存 JS 对象的一种方式,键/值对组合中的键名写在前面并用双引号 "" 包裹,使用冒号 : 分隔,然后紧接着值;多个键值之间使用英文 , 分隔。

package main import ( "encoding/json" "fmt") // Student 学生type Student struct { ID int Gender string Name string} // Class 班级type Class struct { Title string Students []*Student} func main() { c := &Class{ Title: "101", Students: make([]*Student, 0, 200), } for i := 0; i < 2; i++ { // 创建 2 个学生对象 stu := &Student{ Name: fmt.Sprintf("stu%02d", i), Gender: "男", ID: i, } c.Students = append(c.Students, stu) } // JSON 序列化:结构体-->JSON 格式的字符串 data, err := json.Marshal(c) if err != nil { fmt.Println("json marshal failed") return } fmt.Printf("json:%s\n\n", data) // JSON 反序列化:JSON 格式的字符串-->结构体 str := `{"Title":"101","Students":[{"ID":0,"Gender":"男","Name":"stu00"},{"ID":1,"Gender":"男","Name":"stu01"}]}` c1 := &Class{} err = json.Unmarshal([]byte(str), c1) if err != nil { fmt.Println("json unmarshal failed!") return } fmt.Printf("%#v\n", c1)}

1868241-20231012171535111-1438822618.png

2.11 结构体标签 Tag

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。

Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下:

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔

注意事项:为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如,不要在 key 和 value 之间添加空格。

例如我们为 Student 结构体的每个字段定义 json 序列化时使用的 Tag:

package main import ( "encoding/json" "fmt") // Student 学生type Student struct { ID int `json:"id"` // 通过指定 tag 实现 json 序列化该字段时的 key Gender string // json 序列化是默认使用字段名作为 key name string // 全小写字母,表示私有不能被 json 包访问} func main() { s1 := Student{ ID: 1, Gender: "女", name: "张三", } data, err := json.Marshal(s1) if err != nil { fmt.Println("json marshal failed!") return } fmt.Printf("json str:%s\n", data)}

1868241-20231012172130820-139386264.png

2.12 删除 map 类型的结构体子项

package main import "fmt" type student struct { id int name string age int} func main() { ce := make(map[int]student) ce[1] = student{1, "张三", 22} ce[2] = student{2, "李四", 23} fmt.Println(ce) delete(ce, 2) fmt.Println(ce)}

1868241-20231012172459626-321054025.png

参考:http://www.topgoer.com/go%E5%9F%BA%E7%A1%80/%E7%BB%93%E6%9E%84%E4%BD%93.html


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK