结构体
-
自定义类型和别名
package main import "fmt" //使用type可以自定义一个新的类型,格式为type 新类型名 类型,使用这种方法产生的类型在编译后还是存在 type myInt int //类型别名,格式为type 别名=类型,只是相当于另外一种称呼,但是本质还是原来的类型,只在代码里存在,编译后就没有了 //byte就是uint8的别名,rune是int32的别名 type yourInt = int func main() { var n myInt = 100 var m yourInt = 200 fmt.Printf("Type:%T,Value:%d", n, n) //类型为main.myInt 包名.类型 fmt.Printf("Type:%T,Value:%d", m, m) //类型为int }
-
结构体定义和匿名结构体
package main import "fmt" //结构体的声明 type person struct { name string age int gender string hobby []string } func main() { //声明结构体的变量 var ( p person p2 person ) //为结构体变量赋值 p.name = "卡卡" p.age = 24 p.gender = "男" p.hobby = []string{"篮球", "羽毛球"} fmt.Printf("type:%T,value:%+v\n", p, p) //类型就是type定义的类型,使用%+v字段名可以打印出所有信息 fmt.Println(p.name) p2.name = "zq" p.age = 23 fmt.Printf("type:%T,value:%+v\n", p2, p2) //没有初始化的值声明后就是空值 //匿名结构体,直接用var 变量名 struct{}声明一个结构体变量,多用于临时场景 var s struct { x string y int } s.x = "Hello" s.y = 999 fmt.Printf("type:%T,value:%+v\n", s, s) //匿名变量的类型为struc{变量 变量类型...} }
-
结构体指针和初始化
结构体是值类型,所以只有使用指针才能在函数内部修改结构体的值。在初始化结构体时注意以下几点:
必须初始化结构体的所有字段。
初始值的填充顺序必须与字段在结构体中的声明顺序一致。
使用值的初始化方式不能和键值初始化方式混用。package main import "fmt" //结构体是值类型 type person struct { name, gender string } func f1(p person) { p.gender = "女" } func f2(p *person) { // (*p).gender = "未知" p.gender = "未知" //等价于上面的写法,因为go中不支持对指针直接操作,所以会自动获取该指针下存储的值再进行操作 } func main() { //声明结构变量再赋值初始化 var p person p.name = "卡卡" p.gender = "男" f1(p) //因为结构是值类型,所以传入函数的是值的拷贝,函数里修改的只是副本不会影响到原数据 fmt.Println(p) f2(&p) //通过传入指针可以在函数里对原来的值进行修改 fmt.Println(p) //声明结构体同时初始化,使用key-value的方式进行对相应的键赋值,没有赋值的就是默认零值 //使用这种方法赋值时用逗号隔开所有的字段,最后一个字段如果没有挨着}也要加逗号 var p2 = person{ name: "Morain", //没有赋值的key的值为对应类型的零值 } fmt.Printf("%+v\n", p2) //直接用值进行初始化,但必须要注意顺序,以及所有的字段都要赋值,不能遗漏,不能和key-value方式混用 p3 := person{ "Lkaak", "男", } fmt.Printf("%+v\n", p3) //利用new直接获得结构体的指针 var p4 = new(person) p4.name = "zq" //等价于(*p2).name,先对p2取值再做操作 fmt.Printf("%p\n", p4) //p2的内存地址 fmt.Printf("%p\n", &p4) //存p2的指针的内存地址 //初始化的同时利用取地址符号&获得结构体的指针 p5 := &person{ "abc", "女", } fmt.Printf("%p\n", p5) //p2的内存地址 }
-
结构体的内存布局
结构体占用一块连续的内存。并且go中结构体也需要满足内存对齐的规则,具体规则如下:参考博客
结构体的成员变量,第一个成员变量的偏移量为 0。往后的每个成员变量的对齐值必须为编译器默认对齐长度(
#pragma pack(n)
)或当前成员变量类型的长度(unsafe.Sizeof
),取最小值作为当前类型的对齐值。其偏移量必须为对齐值的整数倍
结构体本身,对齐值必须为编译器默认对齐长度(#pragma pack(n)
)或结构体的所有成员变量类型中的最大长度(对齐值),取最大数的最小整数倍作为对齐值
结合以上两点,可得知若编译器默认对齐长度(#pragma pack(n)
)超过结构体内成员变量的类型最大长度时,默认对齐长度是没有任何意义的package main import ( "fmt" "unsafe" ) type test struct { a int8 //八个bit刚好占一个byte b int8 c string //占16byte d int32 } func main() { n := test{ 1, 2, "3", 4, } //a,b,c的内存地址是连续的 fmt.Printf("n.a %p\n", &n.a) fmt.Printf("n.b %p\n", &n.b) fmt.Printf("n.c %p\n", &n.c) //由于内存对齐,string需要占16个字节,大于机器默认对齐长度8,所以对齐值为8,因此偏移量要为8的倍数,所以进行补码 fmt.Printf("n.d %p\n", &n.d) fmt.Printf("size:%d\n", unsafe.Sizeof(n)) //struct占用内存是机器默认对齐长度的整数倍 //空结构体不占用内存 var v struct{} fmt.Println(unsafe.Sizeof(v)) //0 }
-
结构体的构造函数
package main import "fmt" //结构体的构造函数,返回一个结构体变量的函数 type person struct { name string age int } type teacher struct { name string } //构造函数约定使用new开头 //构造函数尽量返回结构体指针,减少程序的内存开销 func newPerson(name string, age int) *person { return &person{ name: name, age: age, } } func newTeacher(name string) teacher { return teacher{ name: name, } } func main() { p1 := newPerson("卡卡", 18) p2 := newPerson("Morain", 99) p3 := newTeacher("Lkaak") fmt.Println(p1, p2, p3) }
-
方法和接受者
方法(Method)
是作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)
。接收者的概念就类似于其他语言中的this
或者self
。package main import "fmt" //方法是一种作用于特定类型变量的函数, //定义格式 // func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { // 函数体 // } type dog struct { name string } func newDog(name string) dog { return dog{ name: name, } } //在函数名前加上接受者就能形成方法 //接受者是调用该方法的具体类型变量,默认使用接受者类型名称的小写字母(dog类型命名为d) func (d dog) bark() { fmt.Printf("%s:汪汪汪", d.name) } func main() { d1 := newDog("www") d1.bark() }
-
值类型接受者和指针类型的接受者
值类型接受者传递值的拷贝,指针类型则传递接受者的地址。下列情况推荐使用指针类型接受者。
- 需要修改接收者中的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
package main import "fmt" //方法是一种作用于特定类型变量的函数, //定义格式 // func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) { // 函数体 // } type dog struct { name string } type person struct { name string age int } func newDog(name string) dog { return dog{ name: name, } } func newPerson(name string, age int) person { return person{ name: name, age: age, } } //在函数名前加上接受者就能形成方法,只有接受者这个类型的变量才能调用这个函数 //接受者是调用该方法的具体类型变量,默认使用接受者类型名称的小写字母(dog类型命名为d) func (d dog) bark() { fmt.Printf("%s:汪汪汪\n", d.name) } //值类型的接受者,函数里的数只是原本值的拷贝,不会影响原先的值 func (p person) ageUp() { p.age++ } //指针类型的接受者,通过指针传内存地址来定位,这样修改的值会影响外面的值 //与值类型的区别就在于接受者是指针类型,调用方式也一样 func (p *person) ageRealUp() { p.age++ } func main() { d1 := newDog("www") d1.bark() p1 := newPerson("卡卡", 24) fmt.Println(p1) p1.ageUp() fmt.Println(p1) //值类型和指针类型的方法都是直接使用变量来调用方法,即使是指针类型的接受者还是用原本的变量而不是地址 p1.ageRealUp() fmt.Println(p1) }
-
自定义类型添加方法
package main import "fmt" //接受者的类型可以是任何类型,但必须是自定义类型,不能用已有的类型 //只能给自己的包里定义的类型添加方法,不能直接给其他包的类型添加方法(可以给其他包的类型用type再定义一次) //MyInt 将int定义为自定义MyInt类型, type MyInt int //SayHello 为MyInt添加一个SayHello的方法 func (m MyInt) SayHello() { fmt.Println("Hello, 我是一个int。") } func main() { var m1 MyInt //m1:=MyInt(100),或者var m1 int = 100。声明MyInt类型的变量,需要类型转换,不然默认为int类型 m1.SayHello() //Hello, 我是一个int。 m1 = 100 fmt.Printf("%#v %T\n", m1, m1) //100 main.MyInt }
-
结构体的匿名字段
匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名。因此匿名和非匿名可以混合使用,只是注意使用了匿名字段的类型不能重复使用,会导致重名。package main import "fmt" //匿名字段只有变量名,默认使用类型名作为字段名 //匿名字段结构体内不同类型的字段只能有一个,但可以匿名和非匿名字段混合使用 type person struct { int string } type city struct { number int string } func main() { //值类型赋值 p1 := person{ 10, "卡卡", } //使用类型名来取字段 fmt.Println(p1.int, p1.string) //键值对赋值 c1 := city{ number: 10, string: "成都", } fmt.Println(c1.number, c1.string) }
-
结构体的嵌套
package main import "fmt" //结构体嵌套,结构体里用其他结构体 type address struct { province string city string } type person struct { name string age int addr address } type workPlace struct { province string city string } //匿名结构体嵌套,可以直接访问内层结构体的字段 type city struct { number int address } type User struct { name string age int address workPlace } func main() { p1 := person{ name: "kaka", age: 24, addr: address{ province: "四川", city: "成都", }, } fmt.Println(p1) fmt.Println(p1.name, p1.addr.city) //匿名字段使用类型名作为字段名称 c1 := city{ number: 100, address: address{ province: "四川", city: "成都", }, } //可以直接访问内层嵌套的结构体的字段 //找的顺序就是先找外层,如果没有就进入内层的匿名结构体中寻找 fmt.Println(c1.number, c1.city) //匿名结构体的字段冲突 u1 := User{ name: "卡卡", age: 18, address: address{ province: "四川", city: "成都", }, workPlace: workPlace{ province: "重庆", city: "梁平", }, } //内部结构体存在重复的字段名,就必须制定具体的结构体名才能访问 fmt.Println(u1.address.city, u1.workPlace.city) }
-
结构体的“嵌套”
package main import "fmt" //结构体模拟实现"继承" //结构体能访问内部结构体的字段,并且能使用内部嵌套结构体类型的方法 //动物 type animal struct { name string } //运动 func (a animal) move() { fmt.Printf("%s在移动\n", a.name) } //狗 type dog struct { feet uint8 animal //dog能访问animal的字段和使用它的方法 } //狗叫,使用匿名结构体的访问,先找外层没有name,再进入animal中找name func (d dog) bark() { fmt.Printf("%s正在叫:汪汪汪~\n", d.name) } func main() { d1 := dog{ feet: 4, animal: animal{ name: "呜呜", }, } fmt.Println(d1) d1.bark() //能直接调用嵌套在内部的结构体类型的方法,类似"继承" d1.move() }
-
结构体的可见性
结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。
结构体的函数方法首字母大写表示公开访问,可以在其他包内使用,小写则私有,只有在当前定义的包内使用
-
结构体与JSON
package main import ( "encoding/json" "fmt" ) //Goz中将结构体变量和JSON字符串实现互相转换 //需要解析的结构体的内部的字段首字母要大写,才能使得json包访问到内部的变量 //结构体的本身的名字不用首字母大写是因为使用其他包的方法传参时已经拷贝进去,但内部需要公开才能够访问 //可以通过使用结构体标签(tag)定义不同时候使用的字段的key,注意格式,key和value之间没有空格,不同的键值对之间相距一个空格 type person struct { Name string `json:"name" db:"name" ini:"name"` Age int `json:"age"` } // func main() { p1 := person{ Name: "卡卡", Age: 24, } //序列化,将结构体变量转换为JSON字符串 //使用json包的方法对结构体变量序列化,结构体内部需要首字母大写来公开 p, err := json.Marshal(p1) if err != nil { fmt.Printf("Marshal failed,err:%v", err) return } //输出时json的标签定义的小写首字母的key fmt.Printf("%s\n", p) //反序列化,将JSON字符串转换为结构体变量,使用tag定义的key来写 str := `{"name":"zq","age":23}` var p2 person //反序列化注意先将字符串转换为字符切片类型,然后第二个参数是结构体指针类型,返回错误类型 err = json.Unmarshal([]byte(str), &p2) //这里还是可以用前一个err,因为没有错误会返回一个nil if err != nil { fmt.Printf("Unmarshal failed,err:%v", err) } //这里输出的是和定义相同的首字母大写的key fmt.Printf("%#v\n", p2) }
-
结构体补充
结构体中如果使用到了map或者silce这类引用类型,要注意在这些字段赋值时最好使用拷贝开辟新的内存后使用copy赋值,否则会导致修改原来为字段赋值的底层数据,就会导致结构体内部变量也会发送改变。
type Person struct { name string age int8 dreams []string } func (p *Person) SetDreams(dreams []string) { p.dreams = dreams } //正确的赋值方法 //func (p *Person) SetDreams(dreams []string) { // p.dreams = make([]string, len(dreams)) // copy(p.dreams, dreams) //} func main() { p1 := Person{name: "小王子", age: 18} data := []string{"吃饭", "睡觉", "打豆豆"} p1.SetDreams(data) // 你真的想要修改 p1.dreams 吗? data[1] = "不睡觉" fmt.Println(p1.dreams) // 被修改为不睡觉 }
-
学生管理系统
package main import ( "fmt" "os" ) //学生管理系统函数版,能够查看/新增/删除学生,使用面向过程的模式 type student struct { id int64 name string } //声明变量,注意map是引用类型需要分配内存后才能用 var ( allStudent map[int64]*student ) //构造函数 func newStudent(id int64, name string) *student { return &student{ id: id, name: name, } } func showAllstudent() { fmt.Println("系统所有的学生信息如下:") for k, v := range allStudent { fmt.Printf("学号:%d,姓名:%s\n", k, v.name) } } func addStudent() { var ( id int64 name string ) fmt.Print("请输入学号:") fmt.Scanln(&id) fmt.Print("请输入姓名:") fmt.Scanln(&name) newStu := newStudent(id, name) allStudent[id] = newStu } func deleteStudent() { var ( deleteId int64 ) fmt.Print("请输入需要删除的学生学号:") fmt.Scanln(&deleteId) delete(allStudent, deleteId) } func main() { allStudent = make(map[int64]*student, 48) for { fmt.Println("欢迎进入学生管理系统") fmt.Println(` 1.查看所有学生 2.新增学生 3.删除学生 4.退出`) fmt.Print("请选择你的操作:") var choice int fmt.Scanln(&choice) fmt.Printf("你选择了选项%d\n", choice) switch choice { case 1: showAllstudent() case 2: addStudent() case 3: deleteStudent() case 4: os.Exit(1) default: fmt.Println("没有该选项") } } }
package main //学生管理系统方法版,使用面向对象的模式 import ( "fmt" "os" ) type student struct { id int64 name string } type class struct { number int student map[int64]*student } func newStudent(id int64, name string) *student { return &student{ id: id, name: name, } } func (c *class) setClass(n int) { c.number = n c.student = make(map[int64]*student, n) } func (c *class) showAllstudent() { for _, v := range c.student { fmt.Printf("姓名:%s,学号:%d\n", v.name, v.id) } } func (c *class) addStudent() { var ( id int64 name string ) fmt.Print("请输入学号:") fmt.Scanln(&id) fmt.Print("请输入姓名:") fmt.Scanln(&name) _, ok := c.student[id] if ok { fmt.Println("已存在该学号") return } stu := newStudent(id, name) c.student[id] = stu } func (c *class) deleteStudent() { var ( deleteid int64 ) fmt.Print("请输入需要删除的学生的学号:") fmt.Scanln(&deleteid) _, ok := c.student[deleteid] if !ok { fmt.Println("不存在该学生") return } delete(c.student, deleteid) fmt.Println("删除成功") } func (c *class) editStudent() { fmt.Print("请输入需要修改的学生的学号:") var n int64 fmt.Scanln(&n) oldstu, ok := c.student[n] if !ok { fmt.Println("不存在该学生") return } fmt.Printf("你需要修改的学生信息为姓名:%s,学号:%d\n", oldstu.name, oldstu.id) fmt.Print("请输入修改后的学生名:") var newname string fmt.Scanln(&newname) c.student[n].name = newname fmt.Println("修改成功") } func main() { fmt.Printf("请输入班级人数:") var n int fmt.Scanln(&n) var ca class ca.setClass(n) for { fmt.Println("欢迎进入学生管理系统") fmt.Println(` 1.查看所有学生 2.添加学生 3.删除学生 4.修改学生姓名 5.退出`) fmt.Printf("请选择你的操作") var choice int fmt.Scanln(&choice) fmt.Printf("你选择了操作%d\n", choice) switch choice { case 1: ca.showAllstudent() case 2: ca.addStudent() case 3: ca.deleteStudent() case 4: ca.editStudent() case 5: os.Exit(1) } } }