开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:结构体声明和使用陷阱】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/626/detail/9669
结构体声明和使用陷阱
内容介绍:
一、结构体声明
二、结构体的字段/属性
一、结构体声明
这部分内容在之前的课程中已经进行过实际应用,如我们上节课解决养猫问题时的这段代码就属于声明结构体部分:
type Cat struct {
Name string
Age int
Color int
Hobby string
}
今天对结构体声明作系统的讲述。其基本语法如下:
1.基本语法
type 标识符/结构体名称 struct {
field1 type
field2 type
}
其中type与struct是关键字,在编程过程中不可修改,而结构体名称属于标识符,可根据编程需求自行选择,如Cat、Student等。下方会写入结构体的字段,字段数目依编程需求而定。写入字段时,第一部分是字段名称,如Name、Age等,第二部分是字段的类型,如int、string等。
2.案例
如以下代码为结构体声明部分:
type Student struct {
Name string
Age int
Score float32
}
其中,Student是结构体名称,其首字母大写,说明可以被其他的包
引用;Name、Age、Score是字段名,其首字母大写,说明其包含
的数据可以被其他包引用,若小写,则说明该数据是私有的,只能在
本包使用,这些规则与之前所学的变量的作用域要求一致。
二、结构体的字段/属性
1. 基本介绍
(1)从概念或名称上看:结构体字段=属性=field,由于教材翻译不一致,可能有这3种名称,但这三者在概念上一致(我们授课过程中统一称为字段)。
(2)字段是结构体的一个组成部分,一般是基本数据类型、数组,也可以是引用类型(map、指针或slice)。如解决养猫问题时Cat结构体的Name string就是基本数据类型,也可以是数组,如猫要进行测试,其包含3个项目的测试分数,故可以定义一个字段Score,它是数组类型,如下:
package main
import (
"fmt"
)
type Cat struct {
Name string
Age int
Color string
Hobby string
Scores [3]int
}
func main() {
var cat1 Cat
fmt.printf("cat1的地址=%p\n",&cat1)
cat1.Name = "小白"
cat1.Age = 3
cat1.Color = "白色"
cat1.Hobby = "吃<・)))><<"
fmt.Println("猫猫的信息如下:")
fmt.Println("name=",cat1.Name)
fmt.Println("age=",cat1.Age)
fmt.Println("color=",cat1.Color)
fmt.Println("hobby=",cat1.Hobby)
}
输出结果:
cat1的地址=0xc0420340c0
cat1=<小白 3 白色 吃<・)))><< [0 0 0]>
猫猫的信息如下:
name= 小白
age= 3
color= 白色
hobby= 吃<・)))><<
结构体声明中该结构体Cat包含了数组的相关字段,而该数组是一个包含3个元素的一维数组,而由于未对数组类型的字段Scores赋值,故输出结果中Scores字段的输出结果中3项测试成绩均为默认值“0”;若此处的数组是二维数组,仍然可以运行并输出。此处不多作赘述。
2. 注意事项和细节说明
(1)字段声明的语法同变量声明的语法一致,声明方法:字段名 字段类型
(2)字段的类型可以为:基本类型、数组或引用类型
(3)在创建一个结构体变量后,如果没有给字段赋值,它都对应一个零值(默认值),其规则同前面所讲的相同:
①bool类型是false;数值(整型及浮点型)是0;字符串是""。
②数组类型的默认值与其元素类型相关,如Scores [3]int对应的是[0 0 0 ];当然,数组类型的默认值也取决于数组的类型,此处的数组类型为int,故默认值为[0 0 0] ,若此处的数组类型为string,则对应的默认值为["" "" ""], 即三个空字符串。
③若该字段的数据类型为指针、slice、map,则默认值为nil,即还没有分配空间。也就是说,若该字段的数据类型为指针、slice、map,要先进行make操作,才可以使用。
案例演示:
package main
import (
"fmt"
)
type Person struct {
Name string
Age int
Scores [5]float64
ptr *int//指针,基本数据空间指针为new
slice [ ]int //切片
map1 map[string]string //map
}
func main() {
//定义一个结构体变量
var p1 Person
//结合上节课所学的结构体变量内存布局示意图可分析得到,前三个结构体变量Name、Age、Score已经可以输出默认值,而后面三个字段ptr、slice、map 均为引用类型,故它们还未指向地址空间,它们的默认值是nil,表示还未分配空间,还不能被使用。
fmt.Println(p1)
//输出结果为< 0 [0 0 0 0 0] [ ] map[ ]>。Name是string类型,显示为空字符串;Age为int型,显示为默认值0;Scores是一维数组,其中包含5个int型的数据,皆显示默认值0;而指针部分显示为< nil>,切片显示为空,map也显示为空,换言之,这三个引用类型的变量虽然显示不同,但实际上均显示默认值nil,表示未分配空间。以下代码可作检验:
if p1.ptr == nil {
fmt.Println("ok1")
}
//输出结果为“ok1”,即结构体变量p1指向指针,由于未经过make,输出结果显示为nil;
if p1.slice == nil {
fmt.Println("ok2")
}
//输出结果为“ok2”,即结构体变量p1指向切片,由于未经过make,输出结果显示为nil;
if p1.map1 == nil {
fmt.Println("ok3")
}
//输出结果为“ok3”,即结构体变量p1指向map,由于未经过make,输出结果显示为nil;
//使用slice,一定要make。若直接输入p1.slice[0] =100
fmt.Println(p1),系统报错。
p1.slice = make([ ]int,10)
p1.slice[0] =100
fmt.Println(p1)
}
//输出结果为< 0 [0 0 0 0 0] [100 0 0 0 0 0 0 0 0 0 ] map[ ]>。原因在于:与原先未对切片make空间时相比,为切片make了10个数据,且对第一个数据赋值为100。
//map与slice相同,也必须要先make,才能使用。若直接输入p1.map["key1"] = "tom~" fmt.Println(p1),则由于未使用make,而直接创建字段,系统报错
p1.map1 = make(map[string ]string)
//此处的空间大小无须设定,只要分配有空间,会随着数据变化自行增长
p1.map["key1"] = "tom~"
fmt.Println(p1)
}
//输出结果为< 0 [0 0 0 0 0] [100 0 0 0 0 0 0 0 0 0 ] map[key1:tom~ ]>。
重点强调:使用slice与map时,一定要先make空间,才能正常使用。
未赋值及未make空间前(不含a步骤),结构体内存布局如下:
经make(图中a步骤)之后,指针、切片和map都会指向一个引用空间,存放其地址。
(4)不同结构体的变量的字段都是独立的,互不影响,一个结构体字段的更改,不影响另外一个。结构体是值类型。
案例说明:
package main
import (
"fmt"
)
type Monster struct {
Name string
Age int
}
func main() {
var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 500
//操作1
monster2 :=monster1 //将monster1赋值于monster2
fmt.Println("monster1=",monster1)
fmt.Println("monster2=",monster2)
//输出结果为:monster1=<牛魔王 500> monster2=<牛魔王 500>,两个输出内容相同
// 操作2
monster2 :=monster1 //结构体是值类型,默认为值拷贝
monster2.Name = "青牛精"
fmt.Println("monster1=",monster1)
fmt.Println("monster2=",monster2)
//输出结果为:monster1=<牛魔王 500> monster2=<青牛精
500>,两个monster输出内容不同,说明将monster2的名字
501>修改,monster1不会发生变化。因为结构体是值类型,将
monster1赋值给monster2时,进行的是值拷贝。若要通过改变monster2以改变monster1,则应进行地址的拷贝,即输入monster2 :=&monster1,但此操作会将monster2变为一个指针,运行时系统可能会报错,后面内容会作讲解。
}
以下为该段代码内存布局的变化的示意图:
说明:
①var monster1 Monster
monster1.Name = "牛魔王"
monster1.Age = 500
定义结构体变量monster1,并为字段赋值,其指向一个结构体空间a。
②monster2 :=monster1
创建一个结构体变量monster2,并指向monster1指向的结构体空间a所拷贝的结构体b。此时monster1与monster2是两个完全独立的结构体。
③monster2.Name = "青牛精"
将monster2的名字改为“青牛精”,并不影响monster1的值。
若要改变monster2的值以改变monster1的值,则要将monster1的地址赋给monster2,让monster2也指向monster1指向的结构体空间a,这一操作会在后面讲述。
本节课要重点掌握:
1)引用类型作为结构体的字段时,一定要make;
2)结构体是值类型,一个变量对一个变量赋值时默认为值拷贝。要通过改变一个变量进而改变另一个变量,那么要传递的应是地址。