结构体:
- 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合
- 结构体可以很好的管理数据,使用结构体可以提高程序的易读性,类似于Java的类一样
- 结构体内成员名必须唯一,可用_补位,支持使用自身类型的指针成员。
- struct {}是一个无元素的结构体类型,通常在没有信息存储时使用。优点是大小为0,不需要内存来存储struct {}类型的值
- struct {} {}是一个复合字面量,它构造了一个struct {}类型的值,该值也是空
- 结构体是值类型,结构体是一种数据类型
结构体的创建与初始化:
创建格式:
type 结构体名 struct {成员名 数据类型
}
逐个初始化:部分初始化也是一样,只写要初始化的成员名即可
变量名 := 结构体名{成员名:成员值,成员名:成员值, // 要加逗号,最后一个也要加逗号
}
顺序初始化:
// 这种方式必须将全部成员初始化
变量名 := 结构体名{成员值1,成员值2}
变量.成员名初始化1:
var 变量名 结构体名变量名.成员名 = 成员值
变量.成员名初始化2:
变量名 := 结构体名2{}变量名.成员名 = 成员值
new方法初始化:
var student *Student = new(Student)
// 可简写为
student := new(Student)
// 可省略*号,Go语言会自动加上
// 指针方式赋值
(*student).Name = "韩信"
演示:
// 结构体创建在函数外面,这样是全局的,如果是定义在函数的里面就是局部的
type Hero struct {Name stringLocation stringGold int64
}func main() {// 逐个初始化、部分初始化h1 := Hero{Name: "韩信", Location: "打野", Gold: 300}fmt.Println(h1)// 顺序初始化要按照成员顺序全部赋值,否则数据类型不一致报错h2 := Hero{"李白", "打野", 300}fmt.Println(h2)// 变量.成员名方式1var h3 Heroh3.Name = "露娜"h3.Location = "打野"h3.Gold = 300// 变量.成员名方式2h4 := Hero{}h4.Name = "不知火舞"h4.Location = "中路"h4.Gold = 300// new方法初始化1var h5 *Hero = new(Hero)fmt.Println(h5)// 简写为h6 := new(Hero)(*h6).Name = "干将莫邪"(*h6).Location = "中路"// 也可以直接去掉星号,编译期间会自动加上*fmt.Println(h6)// new方法初始化2var h7 = &Hero{}(*h7).Name = "干将莫邪"(*h7).Location = "中路"h7.Gold = 300fmt.Println(*h7)// 星号运算,下面这么写会报错, 点的运算大于 *号,相当于*(h7.Name),但这是错误的语法fmt.Println(*h7.Name) // 错误写法fmt.Println((*h7).Name) // 正确写法
}
结构体的所有字段的内存地址在内存中是连续分布的,这样的话,我们可以通过地址的加减来快速找到下一个字段
type Test struct {a, b, c, d int
}func main() {test := Test{1, 2, 3, 4}fmt.Printf("a的地址是:%p \n", &test.a)fmt.Printf("b的地址是:%p \n", &test.b)fmt.Printf("c的地址是:%p \n", &test.c)fmt.Printf("d的地址是:%p \n", &test.d)
}
输出:因为int类型是占8个字节,所以每个变量的内存地址依次加8,而内存地址都是16进制计算的,所以结尾都是0和8(8+8=16 逢16进就是0)
a的地址是:0x1400012c000
b的地址是:0x1400012c008
c的地址是:0x1400012c010
d的地址是:0x1400012c018
如果是指针类型,那么对指针来说,指针本身有个地址,同时也指向另一块地址,那么此时指向的地址不一定是连续的,但他们本身的地址是连续的
func main() {a := 10b := 20c := 30d := 40w := &ax := &by := &cz := &dfmt.Printf("指针w的地址是:%p \n", &w)fmt.Printf("指针x的地址是:%p \n", &x)fmt.Printf("指针y的地址是:%p \n", &y)fmt.Printf("指针z的地址是:%p \n", &z)
}
输出:
指针w的地址是:0x14000114018
指针x的地址是:0x14000114020
指针y的地址是:0x14000114028
指针z的地址是:0x14000114030
结构体字段数据类型如果是:指针、slice、map那么默认值都为nil,还没有分配空间,需要先make才可以使用
type Test struct {name stringage intheight float64ptr *int // 指针slice []int // 切片m map[string]int // map
}func main() {t := Test{}fmt.Println(t.name)fmt.Println(t.age)fmt.Println(t.height)fmt.Println(t.ptr)fmt.Println(t.slice)fmt.Println(t.m)
}
输出:
0
0
<nil>
[]
map[]
仅在字段类型全部赋值时,才可做相等操作
type data struct {x inty map[string]int
}type Student struct {name stringage int
}type A struct {num int
}type B struct {num int
}func main() {d1 := data{x: 100,}d2 := data{x: 100,}//d1和d2是两个变量,就算是都是同一个d1也无法比较,因为data中还有一个y没有被赋值println(d1 == d2) // 无效运算: d1 == d2 (在 data 中未定义运算符 ==)// 两个结构体变量可以使用==或!=运算,但不支持<、>s1 := Student{name: "itzhuzhu", age: 24}s2 := Student{name: "韩信", age: 84}s3 := Student{name: "itzhuzhu", age: 24}fmt.Println("s1 == s3", s1 == s3) // truefmt.Println("s1 == s2", s1 == s2) // false// 同类型的两个结构体变量可以相互赋值var s4 Students4 = s3fmt.Println("s4", s4) // s4 {itzhuzhu 24}var a Avar b B// 两个结构体之间相互转换必须是字段的名字、个数、数据类型都一致,否则报错a = A(b)fmt.Println(a, b) // {0} {0}
}
可使用指针直接操作结构字段,但不能是多级指针,同样可以对结构体取地址
type user struct {name stringage int
}func main() {p := &user{name: "itzhuzhu",age: 20,}fmt.Println("++前 结构体的成员值:", p) p.age++fmt.Println("++后 结构体的成员值:", p) // 结构体不能直接 &user{}取地址,需要有一个变量接收才可以fmt.Println("结构体user的地址:", &p) // 结构体user的地址: 0x14000124018p2 := &p*p2.name = "Jack" // 报错:'p2.name' (类型 'string')的间接引用无效
}
输出:
++前 结构体的成员值: &{itzhuzhu 20}
++后 结构体的成员值: &{itzhuzhu 21}
结构体user的地址: 0x14000114018
type-自定义类型:
使用关键字type定义用于自定义类型,包括基于现有类型创建、结构体、数据类型。byte、rune的源码中就是使用了类型别名,实际上使用byte和使用uint8没有什么区别,相当于起了个外号
type byte = uint8// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
结构体使用type重新定义后Go会认为是新的数据类型,支持强制转换。
type Student struct {name string
}type Stu Student
type integer int // 相当于给int取别名为integer了func main() {var stu1 Studentvar stu2 Stustu1 = stu2 // 报错:无法将 'stu2' (类型 Stu) 用作类型 Studentstu1 = Student(stu2) // 支持使用目标类型强制转换fmt.Println(stu1 == stu2) // 即使是判断是否相等也是错误的,无效运算: stu1 == stu2(类型 Student 和 Stu 不匹配)var i integer = 10var j int = 10i = j // 报错:无法将 'j' (类型 int) 用作类型 integeri = integer(j) // 因为是两种数据类型,所以不支持赋值,只能强制转换
}
多个type定义可合并成组,也可在函数或代码块内定义局部类型
type (user struct { // 结构体name stringage uint}event func(string) // 函数类型
)func main() {u := user{"itzhuzhu", 24}fmt.Println(u)var f event = func(s string) {fmt.Println(s)}f("haha")
}
Tag:
- Tag是一个字符串,以key、value形式存在,用于标记字段说明,可以配合反射使用,以及Json解析。
- Tag是用来对字段进行描述的元数据。尽管它不属于数据成员,但却是类型的组成部分。
- 在运行期,可用反射获取标签信息。常被用作格式校验,数据库关系映射等
- key:不能为空,不能包含、空格、引号、冒号
- value:使用双引号
获取结构体Tag内容:
type User struct {// tag使用反斜杠标注name string `name:"userName"`age int `age:"userAge"`
}func main() {u := User{}user := reflect.TypeOf(u)field := user.Field(0)fmt.Println(field.Tag.Get("name"))field2 := user.Field(1)fmt.Println(field2.Tag.Get("age"))
}
空结构:
空结构struct{ }是指没有字段的结构类型。它比较特殊,是因为无论是其自身,还是作为数组元素类型,其长度都为零
func main() {var a struct{}var b [100]struct{}fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) // 0 0
}
尽管没有分配存储内存,但依然可以操作数组元素,对应切片len、cap属性也正常
var a struct{}
var b [100]struct{}func main() {fmt.Println(unsafe.Sizeof(a), unsafe.Sizeof(b)) // 0 0
}
unsafe.Sizeof可以打印出变量的数据类型字节长度
type Student struct {age int // 8个字节name string // 16个字节
}func main() {stu := Student{}stu.age = 23stu.name = "itzhuzhu"fmt.Println(unsafe.Sizeof(stu.age)) //8fmt.Println(unsafe.Sizeof(stu.name)) //16fmt.Println(unsafe.Sizeof(stu)) // 24
}
空结构对象可用于channel作为事件通知
func main() {exit := make(chan struct{})go func() {fmt.Println("hello, world!")exit <- struct{}{}}()<-exitfmt.Println("end.")
}
匿名字段:
所谓匿名字段是指没有名字,仅有类型的字段
type attr struct {perm int
}
type file struct {name stringint // 仅有类型
}
从编译器角度看,这只是隐式以类型标识作名字的字段。相当于语法糖,可实现面向对象语言中引用基类成员的使用方法
type attr struct {age int
}
type file struct {name stringattr
}func main() {f := file{"itzhuzhu", attr{24}}f = file{name: "itzhuzhu",attr: attr{age: 24,},}f.age = 24 //设置匿名字段成员println(f.age) //读取匿名字段成员
}
结构体与数组:
结构体也可以作为数组的数据类型使用
结构体数组定义:
// 数组需要要指定长度
var 结构体数组名[长度] 结构体类型 = [长度] 结构体类型{}
数组修改结构体成员的值:
结构体数组名[索引].成员=值
演示:
type Student struct {name stringage intaddr string
}func main() {// 定义一个长度为3的数组,数据类型是结构体var arr [3]Student = [3]Student{Student{"韩信", 23, "广州"},Student{"李白", 23, "深圳"},Student{"露娜", 23, "佛山"},}fmt.Println("arr:", arr)fmt.Println("arr[0]:", arr[0])fmt.Println("arr[0].name:", arr[0].name)arr[0].addr = "深圳"fmt.Println("成员值修改后", arr[0])// 通过循环输出结构体数组中的内容for i := 0; i < len(arr); i++ {fmt.Println("通过循环输出结构体数组全部的内容:", arr[i])fmt.Println("通过循环输出结构体数组指定的内容:", arr[i].name)}for key, value := range arr {fmt.Println("key:", key)fmt.Println("value:", value)fmt.Println("打印指定内容:", value.age)}
}
结构体与切片:
和数组的玩法一样,除了定义格式稍微不同
结构体切片定义:
var 结构体数组名[] 结构体类型 = [] 结构体类型{}
切片修改结构体成员的值:
结构体切片名[索引].成员=值
演示:
type Student struct {name stringage intaddr string
}func main() {// 与结构体数组不同的是,切片不用写长度了var arr []Student = []Student{Student{"韩信", 23, "广州"},Student{"李白", 23, "深圳"},Student{"露娜", 23, "佛山"},}fmt.Println("arr:", arr)fmt.Println("arr[0]:", arr[0])fmt.Println("arr[0].name:", arr[0].name)arr[0].addr = "深圳"fmt.Println("成员值修改后", arr[0])// 通过循环输出结构体数组中的内容for i := 0; i < len(arr); i++ {fmt.Println("通过循环输出结构体数组全部的内容:", arr[i])fmt.Println("通过循环输出结构体数组指定的内容:", arr[i].name)}for key, value := range arr {fmt.Println("key:", key)fmt.Println("value:", value)fmt.Println("打印指定内容:", value.age)}// 追加数据arr = append(arr, Student{"干将莫邪", 56, "峡谷"})fmt.Println("追加数据后:", arr)
}
结构体与map:
结构体map的定义:
make(map[key的类型]value的类型)
演示:
type Student struct {name stringage intaddr string
}func main() {m := make(map[int]Student)m[0] = Student{"韩信", 23, "广州"}m[1] = Student{"李白", 24, "广州"}m[2] = Student{"露娜", 25, "广州"}fmt.Println("全部数据:", m)fmt.Println("指定索引数据:", m[0])fmt.Println("指定索引成员数据:", m[0].name)delete(m, 0)fmt.Println("删除后:", m)// 循环打印for key, value := range m {fmt.Println("key:", key)fmt.Println("value:", value)fmt.Println("value.name:", value.name)fmt.Println("value.name:", value.age)}
}
结构体作为函数参数:
在函数中修改结构体成员值,是不会影响到原结构体的
创建格式:
func 函数名(结构体){函数体
}
调用格式:
函数名(结构体)
演示:
type Student struct {name stringage intaddr string
}func main() {stu := Student{"不知火舞", 23, "广州"}test(stu)fmt.Println("调用后:", stu)
}func test(stu Student) {// 修改结构体的成员,不会影响到原结构体stu.age = 999fmt.Println("修改后:", stu)
}
输出:
修改后: {不知火舞 999 广州}
调用后: {不知火舞 23 广州}
求最大学生年龄:
键盘录入学生信息,然后判断年龄最大的学生,打印出年龄最大的学生信息
type Student struct {name stringage intaddr string
}func main() {stu := make([]Student, 3)test(stu)
}func test(stu []Student) {//定义年龄最大的变量max := stu[0].age//定义年龄最大的学生索引var maxIndex intfor i := 0; i < len(stu); i++ {fmt.Printf("请输入第%d个学生的详细信息\n", i+1)//切片是Student的类型,Student中有多个成员,所以要把里面的每个成员都赋值fmt.Scan(&stu[i].name, &stu[i].age, &stu[i].addr)// 如果大于最大年龄就重新赋值,然后把最大年龄的学生索引给定义的变量赋值if stu[i].age > max {max = stu[i].agemaxIndex = i}}fmt.Println(stu[maxIndex])
}
结构体添加方法:
主要结构体类型不一样,方法是可以重名的,算是不同的方法
一般写方法的时候都会将结构体类型写为指针类型的
格式:
// 表示是为某一个结构体添加的方法
func (结构体简称 结构体)方法名 (参数列表)(返回值列表){代码
}
调用格式:
对象名.方法
演示:
func main() {stu := Student{"itzhuzhu", 24, 100}stu.show()fmt.Println("调用show()后:", stu)stu.show2()fmt.Println("调用show2()后:", stu)
}type Student struct {name stringage intscore float64
}// 普通方法是不会改变原来结构体的值的,除非是指针类型
func (student Student) show() {student.age = 222fmt.Println(student)
}// 改成指针类型就会将person的数据改变
func (student *Student) show2() {student.age = 333fmt.Println(student)
}
输出:
{itzhuzhu 222 100}
调用show()后: {itzhuzhu 24 100}
&{itzhuzhu 333 100}
调用show2()后: {itzhuzhu 333 100}