结构体:
- 结构体是由一系列具有相同类型或不同类型的数据构成的数据集合,可以提高程序维护性、易读性,相当于Java中的class
- 结构体内成员名必须唯一,可用
_
补位,支持使用自身类型的指针成员。- struct {}是一个无元素的结构体类型,通常在没有信息存储时使用,优点是大小为0
- 结构体是值类型,结构体是一种数据类型
结构体的创建与初始化:
创建格式:
type 结构体名 struct {
成员名 数据类型
}
实例化
实例化是指在面向对象的编程中,把用类创建对象的过程称为实例化。是将一个抽象的概念类具体到该类实物的过程。
变量名 := 结构体名{}
顺序初始化:
// 这种方式必须将全部成员初始化s
变量名 := 结构体名{成员值1,成员值2}
逐个初始化:部分初始化也是一样,只写要初始化的成员名即可
变量名 := 结构体名{
成员名:成员值,
成员名:成员值, // 要加逗号,最后一个也要加逗号
}
变量.成员名初始化1:
var 变量名 结构体名
变量名.成员名 = 成员值
变量.成员名初始化2:
变量名 := 结构体名2{}
变量名.成员名 = 成员值
new方法初始化:
var student *Student = new(Student)
// 可简写为
student := new(Student)
// 可省略*号,Go语言会自动加上
// 指针方式赋值
(*student).Name = "韩信"
演示:
// 结构体创建在函数外面,这样是全局的,如果是定义在函数的里面就是局部的
type Hero struct {
Name string
Location string
Gold int64
}
func main() {
// 逐个初始化、部分初始化
h1 := Hero{Name: "韩信", Location: "打野", Gold: 300}
fmt.Println(h1)
// 顺序初始化要按照成员顺序全部赋值,否则数据类型不一致会报错
h2 := Hero{"李白", "打野", 300}
fmt.Println(h2)
// 变量.成员名方式1
var h3 Hero
h3.Name = "露娜"
h3.Location = "打野"
h3.Gold = 300
// 变量.成员名方式2
h4 := Hero{}
h4.Name = "不知火舞"
h4.Location = "中路"
h4.Gold = 300
// new方法初始化1
var h5 *Hero = new(Hero)
fmt.Println("new方法初始化1:", h5)
// new方法初始化-简写
h6 := new(Hero)
(*h6).Name = "干将莫邪"
// 也可以直接去掉星号,编译期间会自动加上*
h6.Location = "中路"
fmt.Println("new方法初始化,简写版:", h6)
// new方法初始化2
var h7 = &Hero{}
h7.Name = "干将莫邪"
h7.Location = "中路"
h7.Gold = 300
fmt.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 := 10
b := 20
c := 30
d := 40
w := &a
x := &b
y := &c
z := &d
fmt.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 string
age int
height float64
ptr *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 int
y map[string]int
}
type Student struct {
name string
age 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) // true
fmt.Println("s1 == s2", s1 == s2) // false
// 同类型的两个结构体变量可以相互赋值
var s4 Student
s4 = s3
fmt.Println("s4", s4) // s4 {itzhuzhu 24}
var a A
var b B
// 两个结构体之间相互转换必须是字段的名字、个数、数据类型都一致,否则报错
a = A(b)
fmt.Println(a, b) // {0} {0}
}
可使用指针直接操作结构字段,但不能是多级指针,同样可以对结构体取地址
type user struct {
name string
age int
}
func main() {
p := &user{
name: "itzhuzhu",
age: 20,
}
fmt.Println("++前 结构体的成员值:", p)
p.age++
fmt.Println("++后 结构体的成员值:", p)
// 结构体不能直接 &user{}取地址,需要有一个变量接收才可以
fmt.Println("结构体user的地址:", &p) // 结构体user的地址: 0x14000124018
p2 := &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 Student
var stu2 Stu
stu1 = stu2 // 报错:无法将 'stu2' (类型 Stu) 用作类型 Student
stu1 = Student(stu2) // 支持使用目标类型强制转换
fmt.Println(stu1 == stu2) // 即使是判断是否相等也是错误的,无效运算: stu1 == stu2(类型 Student 和 Stu 不匹配)
var i integer = 10
var j int = 10
i = j // 报错:无法将 'j' (类型 int) 用作类型 integer
i = integer(j) // 因为是两种数据类型,所以不支持赋值,只能强制转换
}
多个type定义可合并成组,也可在函数或代码块内定义局部类型
type (
user struct { // 结构体
name string
age 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 { // 结构体
name string
age uint
}
func main() {
u := User{}
// 获得任意值类型对象
user := reflect.TypeOf(u)
//Field:获取字段 参数表示第几个字段
field := user.Field(0)
// Tag:获取标签
fmt.Println(field.Tag.Get("name"))
// 获取键值对 key:value
field2 := user.Field(1)
fmt.Println(field2.Tag.Get("age"))
}
输出:
userName
userAge
空结构:
空结构struct{ }是指没有字段的结构类型。它比较特殊,无论是其自身,还是作为数组元素类型,其长度都为零
func main() {
var a struct{}
var b [100]struct{}
// 返回操作数在内存中的字节大小
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 = 23
stu.name = "itzhuzhu"
fmt.Println(unsafe.Sizeof(stu.age)) //8
fmt.Println(unsafe.Sizeof(stu.name)) //16
fmt.Println(unsafe.Sizeof(stu)) // 24
}
空结构对象可用于channel作为事件通知
func main() {
exit := make(chan struct{})
go func() {
fmt.Println("hello, world!")
exit <- struct{}{}
}()
<-exit
fmt.Println("end.")
}
匿名字段:
所谓匿名字段是指没有名字,仅有类型的字段
type file struct {
name string
int // 仅有类型
}
从编译器角度看,这只是隐式以类型标识作名字的字段。相当于语法糖,可实现面向对象语言中引用基类成员的使用方法
type attr struct {
age int
}
type file struct {
name string
attr
}
func main() {
f := file{"itzhuzhu", attr{24}}
// 或
f = file{
name: "itzhuzhu",
attr: attr{
age: 26,
},
}
fmt.Println(f.age) //读取匿名字段成员
}
结构体与数组:
结构体也可以作为数组的数据类型使用
结构体数组定义:
// 数组需要要指定长度
数组名:= [长度] 结构体类型{}
演示:
type Student struct {
name string
age int
addr string
}
func main() {
// 定义一个长度为3的数组,数据类型是结构体
arr := [3]Student{
Student{"韩信", 23, "广州"},
// Student可以省略
{"李白", 23, "深圳"},
{"露娜", 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 string
age int
addr string
}
func main() {
// 与结构体数组不同的是,切片不用写长度了
arr := []Student{
{"韩信", 23, "广州"},
{"李白", 23, "深圳"},
{"露娜", 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 string
age int
addr 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 string
age int
addr string
}
func main() {
stu := Student{"不知火舞", 23, "广州"}
test(stu)
fmt.Println("调用后:", stu)
}
func test(stu Student) {
// 修改结构体的成员,不会影响到原结构体
stu.age = 999
fmt.Println("修改后:", stu)
}
输出:
修改后: {不知火舞 999 广州}
调用后: {不知火舞 23 广州}
结构体添加方法:
主要结构体类型不一样,方法是可以重名的,算是不同的方法
一般写方法的时候都会将结构体类型写为指针类型的
格式:
// 表示是为某一个结构体添加的方法
func (变量名 数据类型)方法名 (参数列表)(返回值列表){
代码
}
调用格式:
对象名.方法
演示:
func main() {
stu := Student{"itzhuzhu", 24, 100}
stu.show()
fmt.Println("调用show()后:", stu)
stu.show2()
fmt.Println("调用show2()后:", stu)
}
type Student struct {
name string
age int
score float64
}
// 普通方法是不会改变原来结构体的值的,除非是指针类型
func (s Student) show() {
s.age = 222
}
// 改成指针类型就会将person的数据改变
func (s *Student) show2() {
s.age = 333
}
输出:
调用show()后: {itzhuzhu 24 100}
调用show2()后: {itzhuzhu 333 100}
求最大学生年龄:
键盘录入学生信息,然后判断年龄最大的学生,打印出年龄最大的学生信息
type Student struct {
name string
age int
addr string
}
func main() {
stu := make([]Student, 3)
test(stu)
}
func test(stu []Student) {
//定义年龄最大的变量
max := stu[0].age
//定义年龄最大的学生索引
var maxIndex int
for 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].age
maxIndex = i
}
}
fmt.Println(stu[maxIndex])
}