struct结构体
概述
Go 语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是 Golang 中一种复合类型,它是由一组具有相同或不同类型的数据字段组成的数据结构。
结构体是一种用户自定义类型,它可以被用来封装多个字段,从而实现数据的组合和抽象化。
在 Golang 中,结构体是一种非常灵活和扩展性强的类型,它支持嵌套、组合、方法等高级特性。
类比
Golang的结构体和其他高级语言中的类相似,
1.结构体字段就是类的变量或属性。
2.结构体方法就是类的方法。
3.结构体的嵌套就是类的继承。
4.结构体的组合可以实现类的多态。
结构体的概念和定义
基本语法
结构体定义需要使用 type 和 struct 语句。
struct 语句定义一个新的数据类型,结构体中有一个或多个成员。
type 语句设定了结构体的名称。结构体的格式如下:
//语法结构
type struct_variable_type struct {
member definition
member definition
...
member definition
}
//中文
type 结构体类型名 struct{
成员1 成员类型
成员2 成员类型
成员3 成员类型
...
}
举例
type Person struct{
name string
age int64
}
注意
type
表明结构体本质上就是一个类型,类型名为Person。
结构体字段
结构体的字段可以是任意类型,甚至是结构体本身,也可以是函数或者接口。
如果一个字段在代码中从来不会被用到,那可以把它命名为_
,即空标识符。
大小写问题
结构体中的字段通过首字母大小写来控制私有或公有。
私有属性或方法只能在本包内访问。
公有属性或方法可以跨包访问。
json
标签映射不能访问小写开头的字段是因为标签映射需要借助reflect
包,属于跨包访问。
字段标记举例
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email"`
}
这些标记信息通过反射接口可见,并参与结构体的类型标识,但在其他情况下被忽略。
结构体实例化
字面量初始化
字面量初始化时实例的声明和初始化同时进行。
基本语法
var 变量名 结构体类型 = 结构体类型{
字段1:值,
字段2:值,
}
//简易初始化
var 变量名 = 结构体类型{
字段1:值,
字段2:值,
}
//极简初始化
变量名 := 结构体类型{
字段1:值,
字段2:值,
}
举例
package struct_knowledge
import "fmt"
func CreateStruct(){
type Person struct{
name string
age int64
}
var person = Person{
name:"张三",
}
fmt.Printf("person实例的值为%#v\n",person)
}
结果
person实例的值为struct_knowledge.Person{name:"张三", age:0}
小结
1.实例的最后一定要用,
结尾
var person = Person{
//实例的最后一定要用,结尾
name:"张三",
}
2.声明却没赋值的属性会采用零值
var person = Person{
//实例的最后一定要用,结尾
name:"张三",
//age没有赋值
}
//结果:
person实例的值为struct_knowledge.Person{
name:"张三",
//采用零值
age:0,
}
赋值实例化
访问结构体成员
如果要访问结构体成员(属性和方法),需要使用点号.
操作符,格式为:
结构体实例.成员名
注意是结构体实例
举例
package struct_knowledge
import "fmt"
func CreateStruct(){
type Person struct{
name string
age int64
}
var person = Person{
name:"张三",
}
fmt.Printf("person实例的值为%#v\n",person)
//访问成员
fmt.Printf("person的name值为%#v\n",person.name)
}
结果
person实例的值为struct_knowledge.Person{name:"张三", age:0}
person的name值为"张三"
注意
结构体实例可以访问成员属性和成员变量,结构体类型名是不行的。(结构体类型名就是一个类型)
赋值初始化
基本语法
var 实例名 结构体类型
实例.属性 = 值
实例.属性2 = 值
举例
package struct_knowledge
import "fmt"
func CreateStruct2(){
type Person struct{
name string
age int64
}
var person Person
fmt.Printf("未赋值前,结构体实例的值为%#v\n",person)
person.name = "张三"
person.age = 19
fmt.Printf("赋值后,结构体实例的值为%#v\n",person)
}
结果
未赋值前,结构体实例的值为struct_knowledge.Person{name:"", age:0}
赋值后,结构体实例的值为struct_knowledge.Person{name:"张三", age:19}
小结
1.结构体实例未赋值时,所有字段采用零值。
顺序初始化
我们也可以不写属性名赋值,但是这种必须要保证赋值顺序和结构体声明顺序一致。
顺序初始化时,实例的声明和赋值同时进行,且赋值要与结构体字段一一对应。
package struct_knowledge
import "fmt"
func CreateStruct3(){
type Person struct{
name string
_ bool
age int64
}
var person = Person{"张三",true,19}
fmt.Printf("赋值后,结构体实例的值为%#v\n",person)
}
结果
赋值后,结构体实例的值为struct_knowledge.Person{name:"张三", _:false, age:19}
注意事项
1.结构体中即使是`_`这种表示空标识的字段,在顺序初始化时也得赋值。
2.赋值时值的个数要与字段个数相同,不能多也不能少。
阶段总结
字面量初始化、赋值初始化都可以看作关键字赋值,无需考虑顺序,没有赋值的采用零值。
顺序初始化则是位置赋值,需要与结构体的字段一一对应,赋值个数即不能多,也不能少。
new函数
基本语法
new 函数是 Golang 中的一个内置函数,它用于创建一个指向新分配的类型为T的零值的指针。
在使用 new 函数时,我们需要传递一个类型参数,该参数表示要分配内存的类型。
基本语法
var 结构体实例 = new(结构体类型)
举例
package struct_knowledge
import "fmt"
func CreateStructByNew() {
type Person struct{
name string
age int
}
//new函数实例化
person := new(Person)
fmt.Printf("person的类型为%T\n",person)
person.name = "张三"
person.age = 19
fmt.Printf("赋值后,结构体实例person的值为%#v\n",person)
//普通实例
var person1 Person
fmt.Printf("person的类型为%T\n",person1)
}
结果
person的类型为*struct_knowledge.Person
赋值后,结构体实例person的值为&struct_knowledge.Person{name:"张三", age:19}
person的类型为struct_knowledge.Person
小结
new函数
得到的实例是结构体指针类型,但是由于golang的结构体和数组二者的引用类型赋值时不需要使用*指针类型
取值,所以很容易产生混淆。
new的等价
new函数
其实进行了两步操作
1.声明了一个指针变量
2.分配内存
我们也可以自行声明指针变量,然后用new
分配内存。
声明指针变量
func CreateStruct7(){
type Person struct{
name string
age int
}
/*
只声明了变量,没有分配内存,值为nil
*/
var person1 *Person
fmt.Printf("未分配内存前,person1的值为%#v\n",person1)
//未分配内存前为nil,不能操作
//panic: runtime error: invalid memory address or nil pointer dereference
// person1.age = 19
}
结果
未分配内存前,person1的值为(*struct_knowledge.Person)(nil)
此时我们不能赋值,就和空切片、空map一样,我们还需要分配内存。给指针类型分配内存就需要使用
//得到一个该类型的指针,并且分配了空间
new(类型)
分配空间后,将采用结构体字段的零值。
分配空间举例
package struct_knowledge
import "fmt"
func CreateStruct7(){
type Person struct{
name string
age int
}
/*
只声明了变量,没有分配内存,值为nil
*/
var person1 *Person
fmt.Printf("未分配内存前,person1的值为%#v\n",person1)
//panic: runtime error: invalid memory address or nil pointer dereference
// person1.age = 19
//分配空间
person1 = new(Person)
fmt.Printf("分配内存后,未赋值前,person1的值为%#v\n",person1)
person1.name = "张三"
fmt.Printf("分配内存并且赋值后,person1的值为%#v\n",person1)
}
结果
未分配内存前,person1的值为(*struct_knowledge.Person)(nil)
分配内存后,未赋值前,person1的值为&struct_knowledge.Person{name:"", age:0}
分配内存并且赋值后,person1的值为&struct_knowledge.Person{name:"张三", age:0}
new的字面量赋值
易混淆误区
我们声明了一个指针类型的结构体,我们可以给他直接赋值,例如
type Person struct{
name string
age int
}
var person = new(Person)
//直接赋值
person.name = "张三"
实际上这是因为golang
对结构体和数组做了处理,再给结构体和数组的指针类型直接赋值时不需要使用*指针类型
取内容。
var price = new(int)
//price是*int类型,在修改时为
*price = 15
var lisa = new(Person)
//lisa的类型实际上为 *Person
/*
常规操作
*lisa.name = "张三"
但是golang决定这样可读性太差就去掉了前面的*,
反而增加了我们理解的难度
*/
lisa.name = "张三"
指针类型赋值
由于new函数
得到的是指针类型,所以字面量赋值时就需要。
type Person struct{
name string
age int
}
var person = new(Person)
//需要&取地址获得指针类型
person = &Person{
name:"张三",
age:19
}
小结
由于结构体是值类型,所以我们在使用时要严格区分传递的是否是指针,不要囫囵吞枣。
结构体字段
golang的结构体字段很复杂,值得单独讲解。
基本形式
基本语法
字段名和字段类型
type 结构体名 struct{
字段1 字段类型
字段2 字段类型
...
}
结构体的首字母小写,则该结构体类型只能在本包访问,反之可以跨包访问。
结构体字段的首字母小写,则该结构体字段只能在本包访问,反之可以跨包访问。
举例
type Person struct{
//该字段可以跨包访问
Name string
//该字段不能跨包访问
age int
}
跨包访问
package 包1
//在包1中通过包2访问就叫跨包访问
包2.变量
同类型省略形式
基本语法
type 结构体类型 struct{
字段1 类型A
字段2,字段3 类型B
字段4 类型C
}
等价于
type 结构体类型 struct{
字段1 类型A
字段2 类型B
字段3 类型B
字段4 类型C
}
举例
package struct_knowledge
import "fmt"
func CreateStruct4(){
//同类型字段简写
type Struct1 struct{
IsOk bool
age,price int
name string
}
var Struct1Item = Struct1{true,20,25,"张三"}
fmt.Printf("赋值后,结构体实例的值为%#v\n",Struct1Item)
}
结果
赋值后,结构体实例的值为struct_knowledge.Struct1{IsOk:true, age:20, price:25, name:"张三"}
省略字段名
基本语法
这种省略字段名实际上就是拿类型作为字段名。
类型名虽然是关键字,但是在结构体中允许关键字当字段名。
这是为结构体嵌套做铺垫。
因为嵌套结构体就是将其他结构体类型直接写到该结构体中。
type 结构体类型 struct{
字段1 类型A
类型B
字段1 类型C
}
等价于
type 结构体类型 struct{
字段1 类型A
字段B 类型B
字段1 类型C
}
举例
package struct_knowledge
import "fmt"
func CreateStruct5(){
//省略字段名
type Struct2 struct{
name string
string
bool
age int
}
//相当于
/*
type Struct2 struct{
name string
string string
bool bool
age int
}
*/
var Struct2Item Struct2
Struct2Item.name = "张三"
Struct2Item.bool = true
fmt.Printf("赋值后,结构体实例的值为%#v\n",Struct2Item)
}
结果
赋值后,结构体实例的值为struct_knowledge.Struct2{name:"张三", string:"", bool:true, age:0}
同名字段
在同一个结构体中声明同名字段,会报错,例如
//报错:name redeclared
type Struct1 struct{
name string
name string
}
注意
由于golang严格区分大小写,所以首字母大小写不同或者个别字母大小写不同属于不同字段,例如
//正确,属于不同字段
type Struct1 struct{
name string
Name string
}
字段冲突
之所以会考虑到这个问题,是因为后续结构体嵌套会遇到同名字段问题。
结构体指针
常见方法
常见的获取结构体指针的方式如下:
package struct_knowledge
import "fmt"
//获得指针类型结构体实例的方式
func CreateStruct8(){
type Person struct{
name string
age int
}
/*
方式1:
声明指针类型变量,然后用new分配内存
*/
var person1 *Person
person1 = new(Person)
fmt.Printf("分配内存后,未赋值前,person1的值为%#v\n",person1)
/*
方式2:
声明指针类型变量,然后字面量赋值分配内存
*/
var person2 *Person
person2 = &Person{}
fmt.Printf("分配内存后,未赋值前,person2的值为%#v\n",person2)
/*
方式3:
直接用new函数声明指针类型并分配内存
*/
var person3 = new(Person)
fmt.Printf("分配内存后,未赋值前,person3的值为%#v\n",person3)
/*
方式4:
直接用字面量取地址
*/
var person4 = &Person{}
fmt.Printf("分配内存后,未赋值前,person4的值为%#v\n",person4)
}
结果
分配内存后,未赋值前,person1的值为&struct_knowledge.Person{name:"", age:0}
分配内存后,未赋值前,person2的值为&struct_knowledge.Person{name:"", age:0}
分配内存后,未赋值前,person3的值为&struct_knowledge.Person{name:"", age:0}
分配内存后,未赋值前,person4的值为&struct_knowledge.Person{name:"", age:0}
有个方法可以分辨操作的是否是指针,就是打印时前面是否带
&
构造函数法
所谓构造函数法,就是自定义一个函数,返回结构体指针类型,该方法的命名规范New+结构体类型名
,例如
func NewPerson(name string, age int) *Person {
return &Person{Name: name, Age: age}
}
p := NewPerson("Tom", 25)
实际上就是字面量赋值指针类型。
结构体传参
结构体实例为值类型
由于结构体是值类型
,所以直接传递结构体实例时,在函数内部操作该实例的拷贝对外部原实例没影响。
package struct_knowledge
import "fmt"
//注意需要在包外操作的字段一定要首字母大写
//私有字段无法跨包访问,会提示undefined
type Student struct{
Name string
Age int
}
//结构体是值类型
func StructByVal(st Student){
st.Age = 20
fmt.Printf("函数内该结构体实例的值为%#v\n",st)
}
调用
package main
import (
"fmt"
"go_learn/struct_knowledge"
)
//这是入口文件
func main(){
//验证结构体是值类型
var myStudent struct_knowledge.Student
myStudent.Name = "张三"
struct_knowledge.StructByVal(myStudent)
fmt.Printf("函数外该结构体实例的值为%#v\n",myStudent)
}
结果
函数内该结构体实例的值为struct_knowledge.Student{Name:"张三", Age:20}
函数外该结构体实例的值为struct_knowledge.Student{Name:"张三", Age:0}
由于结构体是值类型,所以函数内的操作无法影响到外面的原始实例。
这也解释了为什么前后端交互传递的都是结构体实例指针,因为需要得到前端传递的值。
使用指针类型传参
举例
package struct_knowledge
import "fmt"
type Student struct{
Name string
Age int
}
//指针类型传递送
func StructByPoint(st *Student){
//golang的数组与结构体不需要显式取内容
st.Name = "里萨"
st.Age = 25
fmt.Printf("函数内该结构体实例的值为%#v\n",st)
}
调用
package main
import (
"fmt"
"go_learn/struct_knowledge"
)
//这是入口文件
func main(){
//指针类型传值
var myStudent= new(struct_knowledge.Student)
myStudent.Name = "张三"
struct_knowledge.StructByPoint(myStudent)
fmt.Printf("函数外该结构体实例的值为%#v\n",myStudent)
}
结果
函数内该结构体实例的值为&struct_knowledge.Student{Name:"里萨", Age:25}
函数外该结构体实例的值为&struct_knowledge.Student{Name:"里萨", Age:25}
传递结构体实例的指针可以修改外部实例。
注意事项
需要我们给结构体指针赋值时不需要使用*
取结构体内容,例如
type Student struct{
Name string
Age int
}
var student = new(Student)
student.Name = "张三"
但是他还是结构体指针类型,当我们需要传参时,如果参数要求的是值不是指针,那我们仍然需要*
取结构体内容。
type Student struct{
Name string
Age int
}
var MyStudent = new(Student)
MyStudent.Name = "张三"
func Add(st Student){
...
}
//由于函数参数要结构体的值,所以需要结构体指针取值
Add(*MyStudent)
相应的如果函数要的是指针,我们就要传递指针,例如
type Student struct{
Name string
Age int
}
//这里是值类型
var MyStudent Student
MyStudent.Name = "张三"
//函数参数要求指针
func Add(st *Student){
...
}
//传递指针类型
Add(&MyStudent)