结构体 Struct
第一个概念:结构体类型怎么表示?
就好像整型类型表示为int,字符串切片类型表示为[]string, key为字符串value为整型的哈希表的类型表示为map[string]int一样,结构体的类型表示为struct{...}
而给结构体赋值的时候,就要写为 “类型 {}”的形式,也就是 “struct{…}{}”的形式。
第二个概念:结构体的字面值怎么表示?
就好像整型的字面值表示为10,字符串切片字面值表示为{“a”, “b”, ”c”},key为字符串value为整型的哈希表的字面值表示为{“a”:1, “b”:2, “c”:3}一样。
结构体的字面值表示为 {字段1:xxx, 字段2:xxx, ...}
第三个概念:不要混淆结构体类型和结构体变量
type employee struct {
name string
level int
}
type student struct {
name string
schoolName string
}
var zbp = employee {name:"zbp", level:3}
这里面 struct{xxx} 和 employee和 student就是一种结构体类型,zbp就是结构体变量。而且employee和student虽然都是结构体类型,但是他们不是相同类型(因为他们用type关键字声明为新类型),因为结构体类型下又有很多很多类型,就好像map哈希表下又很多类型一样,map[string]int是一种map类型,map[string]string又是一种不同的map类型。
如何定义一个结构体变量以及如何访问结构体变量中成员的值呢:
例子1:结构体变量的定义和成员访问
package structExample
import "fmt"
func Run(){
var xiaoming = struct {
name string
age int
hobby []string
education map[string]bool
} {
name: "xiaoming",
age: 12,
hobby: []string{"basketball", "football"},
education: map[string]bool{
"primary" : true,
"medium" : true,
"senior" : false,
},
}
xiaoming.age++
fmt.Println(xiaoming)
fmt.Println(xiaoming.name)
}
可以通过 变量.成员 的方式来访问成员变量。
但是一般我们不会为一个变量赋值的时候才开始去写这个结构体的类型,而是在包级作用域下先将一个我们想使用的结构体类型用type定义为一个新的类型,然后在函数中定义结构体变量的时候就可以用这个新的类型(其底层为结构体类型)进行定义变量。
如下面的例子所示:
package structExample
import "fmt"
type Person struct { // 将这个结构体类型用type定义为Person这个新类型(其实相当于给这个结构体类型起一个别名Person)
name string
age int
hobby []string
education map[string]bool
}
func Run2() {
var xiaoming = Person { // 用Person类型定义一个结构体变量xiaoming
name: "xiaoming",
age: 12,
hobby: []string{"basketball", "football"},
education: map[string]bool{
"primary": true,
"medium": true,
"senior": false,
},
}
var xiaohong = Person {
name: "xiaohong",
age: 18,
hobby: []string{"drawing", "dancing"},
education: map[string]bool{
"primary": true,
"medium": true,
"senior": true,
},
}
fmt.Println(xiaoming, xiaohong)
}
通过这种方式用type将一个结构体类型重新定义为一个新类型的好处就是:我们不用每一次定义一个新的变量时都写一次一长串的struct{....}的内容,例如这里不仅定义了小明,还定义了小红,之后可能还会定义其他的各种格样的人,只用Person类型声明就会少写很多重复代码。
类比以前我们学过的语言,例如php或者python或者js,我们可以发现,strut结构体这种类型很像其他语言中的类,通过type以结构体作为底层类型创建出来的一个新类型(如上例中的Person类型)就相当于创建了一个类。我们可以通过这个类实例化很多个对象(如上面的xiaoming和xiaohong这两个人)。
结构体中所有的成员也同样是变量,我们可以直接对每个成员进行取地址的操作,然后通过指针来直接访问或修改成员的内容。
例如:
var agePoint = &xiaoming.age
*agePoint = 100
fmt.Prinfln(xiaoming.age) // 100
我们在操控结构体的时候,更多时候不是对结构体本身进行,而是对结构体的指针操作从而操控结构体(我的建议是凡是操控结构体都用指针操控而不要直接操控,这样可以避免很多不必要的你可能发现不了的错误,尤其是将结构体传入到一个函数中操作的时候)。
例如:
func Run3() {
var zbp *Person = &Person{
name:"zbp",
age:24,
}
zbp.age = 25 // 通过结构体指针操作结构体
fmt.Println(*zbp)
(*zbp).age = 26 // 直接操作结构体本身,和zbp.age的效果是等价的
fmt.Println(*zbp)
}
这里要注意的是*zbp必须要包一个括号才能使用age成员,否则(go会认为你要操作的是age这个变量的指针而不是zbp的指针)会报错。
*Person表示Person结构体的指针类型,因为指针也是有各种类型的指针的,例如*int是整型变量的指针,*map[string]int表示key为字符串value为整型的哈希表指针,*Person就表示Person结构体指针,Person是一种类型。
*Person是指针类型,*zbp是指针指向的值,请勿混淆。
var zbp *Person = &Person{xxxxx} 等价于 zbp := new(Person); *zbp = Person{xxx}
从上面的例子我们知道,在go中是允许通过结构体指针来访问和修改成员。
能使用指针操作结构体的成员这个功能意义重大,尤其是在把一个结构体传入到函数中的时候,我们一般不会直接把结构体传入函数,而是把结构体指针传入函数。原因是:在Go中,函数的参数传入都是值拷贝,意味着如果你直接传入一个结构体,系统会在内存中重新开辟一块空间,并将传入的结构体拷贝到这个空间,会造成不必要的内存开销。
不过将结构体指针传入函数意味着在函数中改变这个结构体会影响函数外的结构体变量,因为他们都是指向一块空间(当然啦,有时候我们就是希望在函数内的操作能影响到外面的结构体变量)。
函数返回一个结构体的时候,一般也是返回结构体的指针。
定义一个结构体类型的时候,如果相邻的成员类型如果相同的话可以被合并到一行,例如:
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
结构体成员的输入顺序也有重要的意义,看下面的例子:
type employee1 struct {
name string
level int
}
type employee2 struct {
name string
level int
}
func TestStructType() {
e1 := employee1{} // 定义一个字面值为零值的结构体变量e1和e2
e2 := employee2{}
fmt.Println(e1==e2) // 报错,因为e1和e2不是相同类型
}
报错原因很好理解,虽然employee1 和 employee2的底层类型都是相同的
struct{
name string
level int
}
但是由于使用type关键字所以employee1 和 employee2就是两个全新的类型,所以e1和e2不是相同类型不能比较。
我们再看看这样:
func TestStructType() {
e1 := struct {
name string
level int
} {} // 定义一个字面值为零值的结构体变量e1和e2
e2 := struct {
name string
level int
} {}
fmt.Println(e1==e2) // true
}
假如我们换一下e2的结构体类型中的成员顺序,看看结果如何:
func TestStructType() {
e1 := struct {
name string
level int
} {}
e2 := struct {
level int
name string
} {}
fmt.Println(e1==e2) // 报错e1和e2不是相同类型,不能比较
}
看我标红的地方,我把level和name的顺序换了一下,结果e1和e2就不是同一个类型的结构体了。
说明,结构体类型中的成员顺序也很重要,先后顺序不同就是定义了不同的结构体类型。
如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。一个结构体可能同时包含导出和未导出的成员。
一个命名为Employee的结构体类型不能再包含Employee类型的成员:因为一个聚合的值不能包含它自身。(该限制同样适用于数组,即数组的元素不能是数组本身。)但是Employee类型的结构体可以包含*Employee指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。在下面的代码中,我们使用一个二叉树来实现一个插入排序:
# binaryTree.go
package structExample
type BinaryTree struct {
value int // 二叉树根节点的值
left *BinaryTree // 二叉树根节点的左分支,指向的是一个左子树
right *BinaryTree // 二叉树根节点的右分支,指向的是一个右子树
}
// 给一个切片排序(将切片的元素添加到一个二叉树中),返回一个排好序的切片
// 是个导出函数
func Sort(values []int) []int{
// 创建一个空的二叉树,tree是一个空指针,所以tree为nil(因为所有类型指针的零值都是nil)
// 这个二叉树是一个临时的二叉树,每次调用Sort都会创建一个二叉树,返回的排好序的arr后,这个二叉树就销毁
var tree *BinaryTree
// 先将切片的元素存放到二叉树中,存放的过程就是构建二叉树的过程,也是排序的过程
for _, value := range values {
tree = add(value, tree) // 传入二叉树的指针, 这里必须返回tree,因为add外的tree和add内的tree是两个不同的指针了,我之前说过go的函数传参是值拷贝,所以如果不返回一个tree指针,add外的tree就还是一个nil空指针,add返回的tree才是一个指向构建好了的二叉树的指针
}
// 再将二叉树中的元素释放到一个新的切片中,这个新切片中的元素就是排好序的元素
sortedSlice := appendValues(tree, []int{})
return sortedSlice
}
// 往二叉树添加一个元素,add必须返回一个tree
// 是个非导出函数
func add(value int, tree *BinaryTree) *BinaryTree {
if tree == nil { // 如果这棵树没有初始化则初始化
tree = new(BinaryTree) // 在底层创建了一个匿名变量二叉树,并返回二叉树的指针
tree.value = value
return tree
}
if value > tree.value {
tree.right = add(value, tree.right) // 如果树的根节点有值了,而且插入的值比树节点的值大,则将这个值添加到树的右子树中
}
if value <= tree.value {
tree.left = add(value, tree.left) // 同上
}
return tree
}
// 将二叉树中的元素释放都一个切片中,得到的切片其里面的元素就已经排好序的元素
// 是个非导出函数
// values参数是一个空切片,用于接收二叉树释放的元素
func appendValues(tree *BinaryTree, values []int) []int {
if tree == nil{
return values
}
// 使用树的前序遍历算法来遍历树的所有元素(不知道前序遍历的同学可以看看我之前出的数据结构和算法的文章)
values = appendValues(tree.left, values)
values = append(values, tree.value)
values = appendValues(tree.right, values)
return values
}
# main.go
package main
import (
"fmt"
"structExample"
)
func main() {
sortedSlice := structExample.Sort([]int{8,4,7,1,0,14,66,33})
fmt.Println(sortedSlice)
}
结构体类型的零值是每个成员都是零值。通常会将零值作为最合理的默认值。
如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。
有些Go语言程序员用map来模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性。
例如:
urlList := map[string]bool{
"url1":true,
"url2":true,
"url3":false,
}
// 用urlList2代替urlList1可以节省空间,因为struct{}{}完全不占用空间
urlList2 := map[string]struct{}{
"url1":struct{}{},
"url2":struct{}{},
"url3":struct{}{},
}
fmt.Println(urlList,urlList2)
但是因为节约的空间有限,而且语法比较复杂,所以我们通常会避免这样的用法。
结构体字面值
结构体的字面值有两种表示方式:
package structExample
import "fmt"
type employee struct {
name string
level int
}
func Run4() {
// 方式1:按成员声明时的顺序定义
zbp := employee{"zbp", 3}
// 方式2:按成员名称定义成员属性,这样可以不按顺序
weiwei := employee{name:"weiwei", level:10}
fmt.Println(zbp, weiwei)
}
第一种写法,要求以结构体成员定义的顺序为每个结构体成员指定一个字面值。
第二种写法(更常用),以成员名字和相应的值来初始化,可以包含部分或全部的成员。
两种不同形式的写法不能混合使用
结构体的比较
如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的,相等比较运算符==将比较两个结构体的每个成员。
package structExample
import "fmt"
func Run5() {
zbp := employee{"zbp", 3}
zbp2 := employee{"zbp", 3}
fmt.Println(zbp == zbp2)
fmt.Println(zbp.name == zbp2.name && zbp.level == zbp2.level) // 等价于fmt.Println(zbp == zbp2)
}
结构体嵌入和匿名成员
其实结构体嵌入有点类似于其他语言中类的继承,我们看一下下面这种情景:
func Run6() {
type person struct {
name string
age int
}
type student struct {
name string
age int
school string
class string
}
type worker struct {
name string
age int
job string
level int
}
}
这里创建了人,学生和工人这种结构体类型。但是学生和工人也是人,需要重复定义name和age属性,为了优化这一点,我们可以这样:
func Run6() {
type person struct {
name string
age int
}
type student struct {
person
school string
class string
}
type worker struct {
person
job string
level int
}
}
此时,通过将person类型嵌入到student和worker类型中的方式可以让student和worker类型继承perosn类型的所有成员(包括成员属性和成员方法)。此时person就是worker的匿名成员。
这样我们就可以直接对student和worker创建出来的结构体变量的name和age直接赋值。
zbp := new(worker)
zbp.name = "zbp"
zbp.age = 24
zbp.job = "programmer"
zbp.level = 3
fmt.Println(*zbp) // {{zbp 24} programmer 3}
当然此时person也是zbp的一个成员,不过是匿名成员,所以我们也可以通过zbp访问这个匿名成员
fmt.Println(zbp.person) // {zbp 24}
我们也可以通过字面值的方式初始化一个有匿名成员的worker结构体
zbp := worker {
name: "zbp",
age: 24,
job: "programmer",
level: 3,
}
fmt.Println(zbp)
但是这样初始化是错误的,结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法:
zbp := worker {
person: person {
name: "zbp",
age: 24,
},
job: "programmer",
level: 3,
}
// 或者
zbp := worker {
person {"zbp", 24,}, "programmer", 3,
}
fmt.Printf("%#v\n", w) // structExample.worker{person:structExample.person{name:"zbp", age:24}, job:"programmer", level:3}
这两种方式一种是按顺序,一种不按顺序
PS: Printf函数中%v参数包含的#副词,它表示用和Go语言类似的语法打印值。 %v是用来专门打印结构体或者如map/slice这中复杂类型的符号。
有关打印的符号可以参考
https://www.jianshu.com/p/8be8d36e779c
最后再补充一点的就是零值结构体的指针和零值指针,这两者是不同的东西,零值指针是声明了但还没有赋值的指针,它本质就是一个nil;而零值结构体的指针是赋值了的结构体的指针,它指向底层的一个内存空间,空间里面放着一个结构体,只不过结构体的每个成员都被赋予了各自类型的零值。
例如:
func main(){
var zbp Person
point1 := &zbp // 零值Person结构体的指针 打印为 &{ 0}
var point2 *Person // Person结构体类型的零值指针 打印为 nil
fmt.Println(point1, point2)
}
那么到此为止,Go的基本和复合数据类型就已经介绍完毕,下一节就是一些附加的拓展知识。