Go语言基础之结构体
前言
Hey,大家好呀,我是星期八,终于迎来了Go中最大一个知识点,结构体。
在Go,是没有类和对象这个概念的。
是通过结构体的各种操作,模拟出来的像Java,Python之类的面向对象的。
回忆map
之前我们讲过,Go语言的map是键值对的方式存储数据的,就像这样的。
//方式一
var student = map[string]string{
"Name": "张三",
"Age": "18",
}
//方式二
var student2 = make(map[string]string, 10)
student2["Name"] = "张三"
student2["Age"] = "18"
但是这样,似乎有一个弊端,我不知道我有几个key,并且value类型是固定的。
理论来说,key Age
对应的value应该是int类型,还有一些其他微妙的问题,通过map都是不太好解决的。
因为解决这些问题,所以,又引出了结构体这个类型。
前戏补充
在开始结构体之前呢,先看两个奇怪的知识点。
自定义类型
代码
type 自定义类型名 类型名
例:
type NewInt int
完整代码
package main
import "fmt"
type NewInt int
func main() {
var n1 NewInt = 1
fmt.Println(n1)//结果为1
}
如果要是理解的话,可以理解为NewInt
包含了int
的功能。
这里可以把NewInt
当作int
来使用。
注:NewInt
是一个新的类型,它包含int
,并不等于int
。
类型别名
代码
type 类型别名 = 类型名
例:
type Nint = int
完整代码
package main
import "fmt"
type Nint = int
func main() {
var n1 Nint = 1
fmt.Println(n1)//1
}
自定义类型和类型别名区别
可能猛一看,感觉自定义类型和类型别名似乎一样,但是其实是不太一样的。
代码
package main
import "fmt"
type Nint1 int //自定义类型
type Nint2 = int //类型别名
func main() {
var n1 Nint1 = 1
var n2 Nint2 = 1
fmt.Printf("n1类型:%T,n2类型:%T", n1, n2)
}
执行结果。
**结论:**自定义类型真的是自定义类型,类型都变了,类型别名只是类型名变了,但是本质没变。
结构体
Go语言的结构体,相当Java
,Python
等语言中的类,已经不再是简简单单的结构体那么简单了。
结构体属于基本数据类型。
内存图大概如下。
结构体定义
定义结构体需要用到关键字type
和struct
。
语法
type 结构体名 struct {
字段1 字段类型1
字段2 字段类型2
...
}
示例,通过结构体描述一个学生。
type Student struct {
Name string
Age int
Height int
Weight int
phone string
}
**注:**如果字段类型是相同的,可以写在同一行。
type Student struct {
Name string
Age, Height, Weight int
phone string
}
结构体初始化
方式一,赋值时初始化
func main() {
var s1 = Student{
Name: "张三",
Age: 18,
Height: 180,
Weight: 120,
phone: "6666666",
}
fmt.Println(s1)
}
方式二,先声明,后赋值
func main() {
var s1 Student
s1.Name = "张三"
s1.Age = 18
s1.Height = 180
s1.Weight = 120
s1.phone = "66666"
}
两个执行结果。
匿名结构体
有时候我们的函数可能会要求传入一个结构体,但是你又不想定义,就想临时用一下,赶紧传参得了。
这时候可以考虑匿名结构体。
方式一,先声明,后赋值
func main() {
var car struct {
Name string;
CarNum string
}
car.Name = "QQ"
car.CarNum = "京6666"
fmt.Println(car) //{QQ 京6666}
}
方式二,声明+初始化
func main() {
var car = struct {
Name string;
CarNum string
}{
Name: "QQ",
CarNum: "京6666",
}
fmt.Println(car) //{QQ 京6666}
}
两个执行结果。
通过&
方式初始化结构体
通过&
的方式初始化,性能会提高一点,因为返回的是第一个的指针。
但是操作过程跟上述一样,Go已经封装好了。
代码
func main() {
//方式一,等于一个空&结构体在赋值
var s1 = &Student{}
s1.Name = "张三"
//...
//方式二,直接赋值
var s2 = &Student{
Name: "",
Age: 0,
Height: 0,
Weight: 0,
phone: "",
}
//方式三不可以
//var s3 &Student//error
}
使用&
的方式基本跟原来一样,但是方式三不行。
&初始化结构体函数注意事项
如果使用&
的方式,那函数参数也要变一下的。
package main
import "fmt"
type Student struct {
Name string
Age, Height, Weight int
phone string
}
func sayStudent1(s Student) {
fmt.Println(s)
}
func sayStudent2(s *Student) {
//如果穿的是结构体地址,那么接收就需要用*
fmt.Println(s)
}
func main() {
var s1 = Student{
Name: "111",
Age: 0,
Height: 0,
Weight: 0,
phone: "1111",
}
var s2 = &Student{
Name: "2222",
Age: 0,
Height: 0,
Weight: 0,
phone: "2222",
}
sayStudent1(s1)
sayStudent2(s2)
}
执行结果。
关于结构体默认值
代码
func main() {
var s1 = Student{}
fmt.Println(s1)
}
执行结果。
在操作结构体时,即使没有赋值,也会有默认值,所以不用担心会报错。
int
默认值时0,string
默认值是"",等。
引言
在Go中,我们是没有类这个概念的,但是我们有结构体呀。
Go中的结构体,就相当于其他语言的类,基本能实现和其他语言一摸一样的操作。
构造函数
构造函数,跟其他语言一样了,官方理解就是在类实例化时执行的方法,通常用于赋值操作。
但是在Go中,可能不是太一样,需要独立用到一个函数完成。
结构体
type Student struct {
Name string
Age int
phone string
}
构造函数
func NewStudent(name string, age int, phone string) *Student {
return &Student{Name: name, Age: age, phone: phone}
}
//函数尽量采用固定格式 New结构体名
赋值操作
func main() {
var s1 = NewStudent("张三", 18, "1111")
fmt.Println(s1)
}
执行结果
为什么构造函数返回的时结构体指针
通常来说两个原因,第一个原因时传地址性能更高,第二个原因是因为规范,后面的函数绑定结构体也是,更多的是一个规范。
不太用纠结说指针怎么怎么看不懂,对于结构体来说,是不是指针,其实用法都一样。
函数绑定结构体
如果你有其他语言的基础,你可能对于类和对象比较熟悉,传统做法中,是将方法写入类中的。
但是在Go中,采用绑定的方式添加方法。
语法
func (一般用this 要绑定的结构体) 函数名([参数1,参数2...]) [(返回值1,返回值2,...)]{
代码
}
//一般用this,也可以用其他的,this就像形参一样,随便换,用self,用p,用s,都一样的
示例:给Student结构体绑定方法。
func (this Student) say() {
fmt.Printf("我是%v,我今年%v岁了,我的手机号是%v\n", this.Name, this.Age, this.phone)
}
main代码
func main() {
//调用构造方法
var s1 = NewStudent("张三", 18, "1111")
//调用Student绑定的say方法
s1.say()
}
执行结果
有没有感觉有点Java和Python的感觉了,上述可是通过结构体的方式调用方法的,这里就和C区分开了。
在Go中,基本就是通过这些操作,模拟出来面向对象的,相比之下,我更习惯Go的方式,更加灵活。
函数绑定结构体(指针方式)
如果说区别,只是将要修改的 要绑定的结构体 前面加一个*
。
代码
func (this *Student) say() {
fmt.Printf("我是%v,我今年%v岁了,我的手机号是%v\n", this.Name, this.Age, this.phone)
}
执行结果和上述一摸一样。
函数绑定结构体(指针方式和普通方式区别)
通常来说,一般使用指针的方式居多。
嗯…不是居多,是基本都是。
区别
代码一
func (this Student) say1() {
fmt.Printf("我是%v,我今年%v岁了,我的手机号是%v\n", this.Name, this.Age, this.phone)
this.Name = "666"//这里修改了Name为其他值
}
第3行修改了Name
func main() {
//调用构造方法
var s1 = NewStudent("张三", 18, "1111")
//调用Student绑定的say方法
s1.say1()
//打印s1.Name
fmt.Println(s1.Name)
}
第7行又打印了s1.Name
执行结果
???结果没修改,what。
代码二
func (this *Student) say1() {
fmt.Printf("我是%v,我今年%v岁了,我的手机号是%v\n", this.Name, this.Age, this.phone)
this.Name = "666"//这里修改了Name为其他值
}
第一行修改为*
了
func main() {
//调用构造方法
var s1 = NewStudent("张三", 18, "1111")
//调用Student绑定的say方法
s1.say1()
//打印s1.Name
fmt.Println(s1.Name)
}
执行结果
这次可以看到,结果变了,在其他函数修改了Name,影响了整个s1的Name。
结论
-
在使用函数绑定结构体时,也尽可能的将结构体参数整成
*
类型的。 -
一是因为规范,二是因为面向对象本该如此,修改对象的属性,理论来说就应该影响整个对象值。
匿名字段结构体
如果以后再遇到匿名这个词,就把他当作没有名字的意思。
匿名加字段结构体代码。
package main
import "fmt"
type Student struct {
string
int
//string //error:duplicate field string
//int //error:duplicate field string
}
func main() {
var s1 = Student{
"666",
0,
}
fmt.Println(s1)
}
第8行和第9行代码,如果去掉注释会报错。
这就说明了个问题,如果是匿名字段结构体,匿名字段类型是不能重复的,如上述代码所示。
结构体嵌套
结构体嵌套,就如名字一样,一个结构体,嵌套了另外一个结构体。
假设
一个学生的信息,假设有姓名,年龄,性别,这三个字段。
这个学生必定要归属一个班级的,假设这个班级的信息有年级,几班,班主任姓名。
创建结构体
根据上述叙述,我们知道一定是有两个结构体的,至少一个是学生,一个是班级。
班级结构体
type Classes struct {
Grade int //年级
Class int //班级
TeacherName string //班主任姓名
}
学生结构体
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
class Classes //所属班级
}
可以看到第5行代码,结构体的字段类型直接是一个结构体,这就是结构体嵌套、
当一个结构体不能完整描述一个对象时,或者说本来就是独立的对象有关联时,就需要结构体嵌套、
嵌套结构体赋值
方式一,直接赋值嵌套结构体
func main() {
var s1 = Student{
Name: "张三",
Age: 18,
Gender: "男",
class: Classes{
Grade: 2020,
Class: 1,
TeacherName: "张三的老师",
},
}
fmt.Println(s1)
}
方式二,分开赋值
func main() {
var c1 = Classes{
Grade: 2020,
Class: 1,
TeacherName: "张三的老师",
}
var s2 = Student{
Name: "张三",
Age: 18,
Gender: "男",
class: c1,
}
fmt.Println(s2)
}
两次执行结果
其实方式一和方式二的本质是一样的,只不过是方式二将嵌套的结构体单独赋值了而已。
匿名嵌套字段
上述我们的Student
结构体是这样写的。
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
class Classes //所属班级
}
但是其实第5行代码的字段是可以省略的,就像这样。
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
Classes //所属班级
}
但是在赋值时,就要注意了,因为Student
结构体已经没有字段名了,所以就不能使用上述的方式赋值了
需要这种。
func main() {
var s1 = Student{
Name: "张三",
Age: 18,
Gender: "男",
Classes: Classes{
Grade: 2020,
Class: 1,
TeacherName: "张三的老师",
},
}
}
没错,第5行的字段名是Classes
结构体名。
执行结果还是一样的。
补充
上述是直接通过定义变量时就直接赋值了。
其实不管是结构体,还是嵌套结构体,都还有一种方法,就是通过.
的方式赋值。
代码如下
结构体嵌套
默认的结构体嵌套,结构体还是有字段名的。
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
class Classes //所属班级
}
所以赋值代码如下。
func main() {
var s1 Student
s1.Name = "张三"
s1.Age = 18
s1.Gender = "男"
s1.class.Grade = 2020
s1.class.Class = 1
s1.class.TeacherName = "张三的老师"
fmt.Println(s1)
}
第6行代码开始,通过s1找到class这个字段,再根据class找到class具体对应的值进行赋值。
匿名嵌套字段
匿名嵌套字段是没有字段名的,是有一个字段类型。
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
Classes //所属班级
}
所以赋值跟上述也不太一样,是这样的。
func main() {
var s1 Student
s1.Name = "张三"
s1.Age = 18
s1.Gender = "男"
s1.Classes.Grade = 2020
s1.Classes.Class = 1
s1.Classes.TeacherName = "张三的老师"
fmt.Println(s1)
}
通过s1直接找到Classes这个结构体,再根据这个结构体找到里面具体的值,进行赋值。
其实跟定义变量时赋值相似。
但是终究执行结果,还是一样的,只是赋值形式不同。
结论
根据嵌套结构体和匿名嵌套结构体再赋值时可以发现。
如果嵌套结构体有字段名,通过字段名找具体的字段,进行赋值。
如果是嵌套结构体匿名字段,通过嵌套结构体的名字,找具体字段,进行赋值。
嵌套结构体字段冲突
这个冲突的问题,其实还是比较少见的,这个问题通常情况下,只会出现在匿名嵌套场景中。
还是上述的结构体,但是赋值可以是这样操作的。
func main() {
var s1 Student
s1.Name = "张三"
s1.Age = 18
s1.Gender = "男"
s1.Classes.Grade = 2020
s1.Classes.Class = 1
s1.Classes.TeacherName = "张三的老师"
//######### 分割 ##########
s1.Grade = 2020 //省去了Classes
s1.Class = 1 //省去了Classes
s1.TeacherName = "张三的老师" //省去了Classes
fmt.Println(s1)
}
第10行,直接通过s1.Grade
赋值,其实是省去了一个Classes
,但是这种操作,只有在匿名嵌套结构体中可以使用。
但是如果我将结构体改成这样子。
//班级
type Classes struct {
Grade int //年级
Class int //班级
TeacherName string //班主任姓名
}
//课程
type Course struct {
CourseName string //课程名字
TeacherName string //任课老师姓名
}
//学生
type Student struct {
Name string //姓名
Age int //年龄
Gender string //性别
Classes //所属班级
Course //任课老师
}
Student
结构体有两个匿名嵌套结构体,一个是Classes
,一个是Course
。
但是有一个字段,是冲突的,就是TeacherName
,如果还是通过懒的方式赋值,会发生什么呢?
func main() {
var s1 Student
s1.Name = "张三"
s1.Age = 18
s1.Gender = "男"
s1.Grade = 2020
s1.Class = 1
s1.TeacherName = "张三的老师"
fmt.Println(s1)
}
第8行,直接找TeacherName
字段,这时候就会出问题了。
意思很简单,就是不知道是Classes
的TeacherName
还是Course
的TeacherName
。
这时候,就必须要指定了。
s1.Classes.TeacherName = "张三的班主任"
s1.Course.TeacherName = "张三的任课老师"
结构体继承
说起继承,学过Java,Python的肯定都不陌生,但是Go中,可没有这个东西呐。
那咋办呢???,还是得用结构体来实现。
假装我们都是男孩,喜欢车,那我们就拿车来举例子吧。
车结构体
//车
type Car struct {
Brand string //车品牌
CarNum string //车牌号
Tyre int //轮胎个数
}
//给车绑定一个方法,说明车的基本信息
func (this *Car) carInfo() {
fmt.Printf("品牌:%s,车牌号:%s,轮胎个数:%d\n", this.Brand, this.CarNum, this.Tyre)
}
宝马车
//宝马车
type BMWCar struct {
//*Car和Car基本没有区别,一个存的是整个结构体,一个存的是结构体地址,用法大同小异
*Car //这就表示继承了Car这个结构体
}
比亚迪车
//比亚迪车
type BYDCar struct {
*Car
}
可能看到这,你会有种熟悉得感觉,这不就是上节课所将的结构体嵌套吗???
这跟继承有毛关系?
其实在Go中,结构体既可以用来存储数据,也可以用来模仿对象的各种操作。
main代码
func main() {
//一个宝马对象
var bmw1 = BMWCar{&Car{
Brand: "宝马x8",
CarNum: "京666",
Tyre: 4,
}}
//一个比亚迪对象
var byd1 = BYDCar{&Car{
Brand: "比亚迪L3",
CarNum: "京111",
Tyre: 4,
}}
//因为 BMWCar 和 BYDCar 都继承了Car,所以都有carInfo这个方法
bmw1.carInfo()
byd1.carInfo()
}
执行结果
这就是一个最简单的,面向对象,跟其他语言一样,继承会将所有的属性和方法都继承过来。
序列化
到此为止呢,结构体基本可以告一段落了,基本算是入门了,当然,并没有结束,但是我想大家都累了,换个方向继续玩。
这个东西叫做序列化,什么意思呢,就是像咱们的切片了,map了,结构体了等,这些都是Go的类型。
如果要和其他语言交流,人家可没有这些玩意唉,那怎么办呢???
众多大佬就形成了一个规范,json
数据格式,json
数据必须是字符串类型。
最外面是'
号,键/值对组合中的键名写在前面并用双引号""
包裹。
就像这样。
'{"Gender":"男","Name":"张三"}' //'说明这个是字符串,一般打印时不显示
序列化我们用到的是json
模块的Marshal
方法。
切片序列化
单独的切片序列化用的很少,但是仍然还是要知道。
示例代码
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Gender string
Name string
}
func main() {
var StudentList = []string{"张三", "李四"}
fmt.Printf("StudentList类型:%T\n", StudentList) //[]string,这是列表类型
serializeByte, err := json.Marshal(StudentList)
if err != nil {
fmt.Println("序列化失败")
return
}
var serializeStr = string(serializeByte)
fmt.Printf("serializeStr类型:%T\n", serializeStr) //string,这是字符串类型
fmt.Printf("serializeStr值:%v\n", serializeStr) //["张三","李四"]
}
第16行代码将切片序列化,但是返回的是[]byte
类型,第21行代码将[]byte
类型转成字符串。
执行结果
map序列化
字典序列化,就比较有味道了,序列化的是一个标准的json
数据格式。
示例代码
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Gender string
Name string
}
func main() {
var StudentInfo = map[string]string{
"Name":"张三",
"Age":"18",
"Gender":"男",
}
fmt.Printf("StudentInfo类型:%T\n",StudentInfo)
serializeByte, err := json.Marshal(StudentInfo)
if err != nil {
fmt.Println("序列化失败")
}
var serializeStr = string(serializeByte)
fmt.Printf("serializeStr类型:%T\n", serializeStr) //string,这是字符串类型
fmt.Printf("serializeStr值:%v\n", serializeStr) //{"Age":"18","Gender":"男","Name":"张三"}
}
执行结果
这个就有点像标准的json
格式了。
结构体序列化
结构体代码
type Student struct {
Name string
Gender string
Age int
}
main
func main() {
var s1 = Student{
Name: "张三",
Gender: "男",
Age: 18,
}
fmt.Printf("StudentInfo类型:%T\n", s1)
serializeByte, err := json.Marshal(s1)
if err != nil {
fmt.Println("序列化失败")
}
var serializeStr = string(serializeByte)
fmt.Printf("serializeStr类型:%T\n", serializeStr) //string,这是字符串类型
fmt.Printf("serializeStr值:%v\n", serializeStr)
}
执行结果
切片套结构体
一般情况下,这种方式数据格式是用的比较多的。
当然, 还可以切片嵌套map,方法和此方法一样,不做例子了。
示例代码
package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Name string
Gender string
Age int
}
func main() {
var s1 = Student{
Name: "张三",
Gender: "男",
Age: 18,
}
var s2 = Student{
Name: "李四",
Gender: "女",
Age: 16,
}
//一个存放 Student 的列表
var studentList = []Student{s1, s2}
fmt.Printf("StudentInfo类型:%T\n", studentList)
serializeByte, err := json.Marshal(studentList) //main.Student
if err != nil {
fmt.Println("序列化失败")
}
var serializeStr = string(serializeByte)
fmt.Printf("serializeStr类型:%T\n", serializeStr) //string,这是字符串类型
fmt.Printf("serializeStr值:%v\n", serializeStr)
}
执行结果
结构体标签(Tag)
Tag
可以理解为结构体的说明,由一对反引号包裹起来。
但是一般情况下,Tag在序列化是用的比较多。
结构体代码
type Student struct {
Name string `json:"name"`
Gender string `json:"gender"`
Age int `json:"age"`
}
每个字段后面跟的,就是Tag,一定不要把格式搞错啦。
main代码
func main() {
var s1 = Student{
Name: "张三",
Gender: "男",
Age: 18,
}
fmt.Printf("StudentInfo类型:%T\n", s1)
serializeByte, err := json.Marshal(s1) //main.Student
if err != nil {
fmt.Println("序列化失败")
}
var serializeStr = string(serializeByte)
fmt.Printf("serializeStr类型:%T\n", serializeStr) //string,这是字符串类型
fmt.Printf("serializeStr值:%v\n", serializeStr)
}
执行结果
可以发现key成小写的了,这就说明一个问题。
在序列化时,如果结构体有json
这个Tag,序列化时就会以json
Tag为准,如果没有json
Tag,则以结构体字段为准。
总结
一定要在下面多多练习,如果在操作过程中有任何问题,记得下面留言,我们看到会第一时间解决问题。
我是码农星期八,如果觉得还不错,记得动手点赞一下哈。
感谢你的观看。