文章目录
- 一、结构体定义
- 二、初始化结构体
- 三、结构体的访问
- 四、结构体指针
- 五、结构体可见性
- 六、结构体标签
- 七、结构体嵌套
- 八、结构体方法
- 九、结构体特性
Go语言中提供了对struct的支持,struct,中文翻译称为结构体,与数组一样,属于复合类型,并非引用类型。
Go语言的struct,与C语言中的struct或其他面向对象编程语言中的类(class)类似,可以定义字段(属性)和方法,但也有很不同的地方,需要深入学习,才能区分他们之间的区别。
一、结构体定义
Go语言中数组可以存储同一类型的数据,但在结构体中我们可以为不同项定义不同的数据类型。
结构体是由一系列具有相同类型或不同类型的数据构成的数据集合
结构体成员是由一系列的成员变量构成,这些成员变量也被称为“字段”
type Member struct {
id int
name, email string
gender, age int
}
上面的代码中,我们定义了一个包含5个字段的结构体,可以看到,相同类型name和email、gender和age在同一行中定义,但比较好的编程习惯是每一行只定义一个字段,如:
type Member struct {
id int
name string
email string
gender int
age int
}
当然,结构体也可以不包含任何字段,称为空结构体,struct{}表示一个空的结构体,注意,直接定义一个空的结构体并没有意义,但在并发编程中,channel之间的通讯,可以使用一个struct{}作为信号量。
ch := make(chan struct{})
ch <- struct{}{}
二、初始化结构体
上面的例子中,我们定义了Member结构体类型,接下就可以这个自定义的类型创建变量了。
- 直接定义变量
直接定义变量,这个使用方式并没有为字段赋初始值,因此所有字段都会被自动赋予自已类型的零值,比如name的值为空字符串"",age的值为0。
var m1 Member//所有字段均为空值
另外也有使用字面量创建变量,这种使用方式,可以在大括号中为结构体的成员赋初始值,有两种赋初始值的方式:
- 按照顺序提供初始化值:
var m2 = Member{
1,
"杰哥的技术杂货铺",
"jiege@163.com",
1,
18,
}
或者
// 简短变量声明方式:
m2 := Member{
1,
"杰哥的技术杂货铺",
"jiege@163.com",
1,
18
}
这种方式要求所有的字段都必须赋值,因此如果字段太多,每个字段都要赋值,会很繁琐,另一种则使用字段名为指定字段赋值,如下面代码中变量m3的创建,使用这种方式,对于其他没有指定的字段,则使用该字段类型的零值作为初始化值。
- 通过field:value的方式初始化,这样可以任意顺序
var m3 = Member{
id:2,
"name":"杰哥的技术杂货铺"
}
或者
// 简短变量声明方式:
m3 := Member{
id:2,
"name":"杰哥的技术杂货铺"
}
三、结构体的访问
通过变量名,使用符号点(.),可以访问结构体类型中的字段,或为字段赋值,也可以对字段进行取址(&)操作。
package main
import "fmt"
//定义结构体
type Persion struct {
name string
age int
sex string
address string
}
func main() {
//1.方法一
var p1 Persion
p1.name = "杰哥的技术杂货铺"
p1.age = 18
p1.sex = "男"
p1.address = "技术博客"
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p1.name,p1.age,p1.sex,p1.address)
//2.方法二
p2 := Persion{}
p2.name = "黄风怪"
p2.age = 3000
p2.sex = "男"
p2.address = "黄风洞"
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p2.name,p2.age,p2.sex,p2.address)
//3.方法三:创建结构体对象时,直接进行赋值
p3 := Persion{name: "蜘蛛精",age: 500,sex: "女",address: "盘丝洞"}
fmt.Printf("姓名:%s,年龄:%d,性别:%s,地址:%s\n",p3.name,p3.age,p3.sex,p3.address)
p4 := Persion{
name:"黄眉大王",
age: 1000,
sex: "男",
address: "小雷音寺",
}
fmt.Println(p4)
//4.方法四:创建结构体对象时,不写字段名,直接赋予数值
//此种方式需要注意顺序
p5 := Persion{"白骨精",300,"女","白骨洞"}
fmt.Println(p5)
}
四、结构体指针
结构体与数组一样,都是值传递,比如当把数组或结构体作为实参传给函数的形参时,会复制一个副本,所以为了提高性能,一般不会把数组直接传递给函数,而是使用切片(引用类型)代替,而把结构体传给函数时,可以使用指针结构体。
指针结构体,即一个指向结构体的指针,声明结构体变量时,在结构体类型前加*号,便声明一个指向结构体的指针,如:
注意,指针类型为引用类型,声明结构体指针时,如果未初始化,则初始值为nil,只有初始化后,才能访问字段或为字段赋值。
var m1 *Member
m1.name = "杰哥的技术杂货铺"//错误用法,未初始化,m1为nil
m1 = &Member{}
m1.name = "杰哥的技术杂货铺"//初始化后,结构体指针指向某个结构体地址,才能访问字段,为字段赋值。
另外,使用Go内置new()函数,可以分配内存来初始化结构休,并返回分配的内存指针,因为已经初始化了,所以可以直接访问字段。
var m2 = new(Member)
m2.name = "杰哥的技术杂货铺"
五、结构体可见性
上面的例子中,我们定义结构体字段名首字母是小写的,这意味着这些字段在包外不可见,因而无法在其他包中被访问,只允许包内访问。
下面的例子中,我们将Member声明在member包中,而后在main包中创建一个变量,但由于结构体的字段包外不可见,因此无法为字段赋初始值,无法按字段还是按索引赋值,都会引发panic错误。
package member
type Member struct {
id int
name string
email string
gender int
age int
}
package main
fun main(){
var m = member.Member{
1,
"杰哥的技术杂货铺",
"jiege@163.com",
1,
18
}//会引发panic错误
}
因此,如果想在一个包中访问另一个包中结构体的字段,则必须是大写字母开头的变量,即可导出的变量,如:
type Member struct {
Id int
Name string
Email string
Gender int
Age int
}
六、结构体标签
在定义结构体字段时,除字段名称和数据类型外,还可以使用反引号为结构体字段声明元信息,这种元信息称为Tag,用于编译阶段关联到字段当中,如我们将上面例子中的结构体修改为:
type Member struct {
Id int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Gender int `json:"gender,"`
Age int `json:"age"`
}
上面例子演示的是使用encoding/json包编码或解码结构体时使用的Tag信息。
Tag由反引号括起来的一系列用空格分隔的key:"value"键值对组成,如:
Id int json:"id" gorm:"AUTO_INCREMENT"
七、结构体嵌套
结构体嵌套,可以理解为定义一个结构体中,其字段可以是其他的结构体,这样,不同的结构体就可以共用相同的字段。
注意, 结构体不能包含自身,但可能包含指向自身的结构体指针。
package main
import (
"fmt"
)
//1.定义一个书的结构体
type Book struct {
bookname string
price float64
}
//2.定义学生的结构体
type Student struct {
name string
age int
book Book
}
type Student2 struct {
name string
age int
book *Book //book结构体的地址
}
func main() {
b1 := Book{}
b1.bookname = "西游记"
b1.price = 66.6
s1 := Student{}
s1.name = "红孩儿"
s1.age = 18
s1.book = b1 //值传递
fmt.Println(b1)
fmt.Println(s1)
fmt.Printf("学生姓名:%s,学生年龄:%d,看的书是:《%s》,书的价格是:%.2f\n",s1.name,s1.age,s1.book.bookname,s1.book.price)
s2 := Student{name: "武松",age: 28,book: Book{bookname: "《Go语言从入门到放弃》",price: 88.8}}
fmt.Println(s2.name,s2.age)
fmt.Println("\t",s2.book.bookname,s2.book.price)
s3 := Student{
name: "jack",
age: 17,
book: Book{
bookname: "十万个为啥",
price: 35.5,
},
}
fmt.Println(s3.name,s3.age)
fmt.Println("\t",s3.book.bookname,s3.book.price)
b4 := Book{bookname: "射雕英雄传",price: 76.0}
s4 := Student2{name:"张三",age: 20,book: &b4}
fmt.Println(b4)
fmt.Println(s4)
fmt.Println("\t",s4.book)
s4.book.bookname = "挪威的森林"
fmt.Println(b4)
fmt.Println(s4)
fmt.Println("\t",s4.book)
}
可以看到,我们定义Student结构体时,可以把Book结构体作为Student的字段。
八、结构体方法
在Go语言中,将函数绑定到具体的类型中,则称该函数是该类型的方法,其定义的方式是在func与函数名称之间加上具体类型变量,这个类型变量称为方法接收器,如:
注意,并不是只有结构体才能绑定方法,任何类型都可以绑定方法,只是我们这里介绍将方法绑定到结构体中。
func setName(m Member,name string){//普通函数
m.Name = name
}
func (m Member)setName(name string){//绑定到Member结构体的方法
m.Name = name
}
从上面的例子中,我们可以看出,通过方法接收器可以访问结构体的字段,我们可以任意命名方法接收器。
调用结构体的方法,与调用字段一样:
m := Member{}
m.setName("小明")
fmt.Println(m.Name)//输出为空
上面的代码中,我们会很奇怪,不是调用setName()方法设置了字段Name的值了吗?为什么还是输出为空呢?
这是因为,结构体是值传递,当我们调用setName时,方法接收器接收到是只是结构体变量的一个副本,通过副本对值进行修复,并不会影响调用者,因此,我们可以将方法接收器定义为指针变量,就可达到修改结构体的目的了。
func (m *Member)setName(name string){//将Member改为*Member
m.Name = name
}
m := Member{}
m.setName("小明")
fmt.Println(m.Name)//小明
方法和字段一样,如果首字母为小写,则只允许在包内可见,在其他包中是无法访问的,因此,如果要在其他包中访问setName
,则应该将方法名改为SetName
由此我们可以看出,要想改变结构体内容时就需要使用指针接收者。
那什么时候该使用值接收者,什么时候使用指针接收者呢,可归纳为以下几点:
- 要更改内容的时候必须使用指针接收者
- 值接收者是go语言特有,因为它函数传参过程是通过值的拷贝,因此需要考虑性能问题,结构体过大也需要考虑使用指针接收者
- 一致性:如果有指针接收者,最好都使用指针接收者
- 值/指针接收者均可接受值/指针,定义方法的人可以随意改动接收者的类型,这并不会改变调用方式
九、结构体特性
下面总结几点结构体的相关特性:
- 值传递
结构体与数组一样,是复合类型,无论是作为实参传递给函数时,还是赋值给其他变量,都是值传递,即复一个副本。
- 没有继承
Go语言是支持面向对象编程的,但却没有继承的概念,在结构体中,可以通过组合其他结构体来构建更复杂的结构体。
- 结构体不能包含自己
一个结构体,并没有包含自身,比如Member中的字段不能是Member类型,但却可能是*Member。