文章目录


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。