本文参考:https://www.liwenzhou.com/posts/Go/10_struct/
结构体
Go语言中的基本数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或者部分属性时,这时候再用单一的基本数据类型明显就无法满足需求。Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,struct 。
Go语言中通过struct 来实现面向对象。
结构体的定义
使用type 和struct 关键字来定义结构体
type 类型名 struct{
字段名 字段类型
字段名 字段类型
...
}
// 类型名: 标识自定义结构体的名称,在同一个包内不能重复
// 字段名: 标识结构体字段名。结构体中的字段名必须唯一
// 字段类型: 标识结构体字段的具体类型
示例:
type person struct{ // 定义一个person的结构体
name string
age int8
city string
}
同类型的子弹也可以写在一行:
type person struct{
name,city string
age int8
}
这样就拥有一个person 的自定义类型,它有name ,age ,city 三个字段,分别标识姓名,年龄和城市。这样我们使用这个person 结构体就能够很方便的在程序中表示和存储人的信息。
语言内置的基本数据类型是用来描述一个值的,而及饿哦固体是用来描述一组值的。比如一个人的名字,年龄,城市等,本质上是一种聚合性的数据类型。
结构体实例化
只有当结构体实例化时,才会真正地分配内存。也就是必须实例化后才能使用结构体的字段。
结构体本身也是一种类型,我们可以像声明内置类型一样使用var 关键字来声明结构体类型
var 结构体实例 结构体类型
基本实例化
type person struct{
name string
age int8
city string
}
func main(){
var p person
p.name = "Negan"
p.age = 18
p.city = "西安"
fmt.Printf("p=%v\n",p) // p={Negan 西安 18}
fmt.Printf("p=%#v\n",p) // p=main.person{name:"Negan",age:18,city:"西安"}
}
我们通过. 来访问结构体字段(成员变量),例如p.name 和p.age 等。
匿名结构体
在定义一些临时数据结构等常见下可以使用匿名结构体
func main(){
var user struct{name string;age int}
user.name = "Negan"
user.age = 18
fmt.Printf("%#v\n", user)
}
创建指针类型结构体
通过使用new 关键字对结构体进行实例化,得到的是结构体地址。
var p = new(person)
fmt.Printf("%T\n",p) // *main.person
fmt.Printf("p=%#v\n",p) // p=&main.person{name:"",age:0,city:""}
从打印结果来看,此时p 是一个结构体指针。
在Go语言中支持对结构体指针直接使用. 来访问结构体成员。
var p = new(person)
p.name = "Negan"
p.age = 68
p.city = "亚历山大"
fmt.Printf("p=%#v\n",p) // p=&main.person{name:"Negan",age:68,city:"亚历山大"}
取结构体的地址实例化
使用& 对结构体进行取地址操作相当于对该结构体类型进行了一次new 实例化操作。
p := &person{}
fmt.Printf("%T\n",p) // *main.person
fmt.Printf("p=%v\n",p) // p=&main.person{name:"",age:0,city:""}
p.name = "Negan"
p.age = 68
p.city = "救世堂"
fmt.Printf("p=%#v\n",p) // p=&main.person{name:"Negan",age:68,city:"救世堂"}
p.name="Negan" 其实在底层是(*p3).name="Negan" ,这是Go语言帮我们实现的语法糖。
结构体初始化
没有初始化的结构体,其成员变量都是对应其类型的零值。
type person struct{
name string
age int8
city string
}
func main(){
var p person
fmt.Printf("p=%#v\n",p) // p=main.person{name:“”,age:0,city:""}
}
使用键值对初始化
使用键值对对结构体进行初始化,键对应结构体的字段,值对应该字段的初始值。
p := person{
name:"Negan",
age:68,
city:"亚历山大"
}
fmt.Printf("p=%#v\n",p) // p=main.person{name:"Negan",age:68,city:"亚历山大"}
也可以使用结构体指针进行键值对初始化
p := &person{
name:"Negan",
age:68,
city:"亚历山大"
}
fmt.Printf("p=%#v\n",p) //p=&main.person{name:"Negan",age:68,city:"亚历山大"}
当某些字段没有初始值的时候,该字段可以不写,此时没有指定初始值的字段的值就是该字段类型的零值。
p := &person{
city:"救世堂"
}
fmt.Printf("p=%#v\n",p) // p=&main.person{name:"",age:0,city:"救世堂"}
使用值的列表初始化
初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值
p := &person{
"Negan",
68,
"救世堂"
}
fmt.Printf("p=%#v\n",p) // p=&main.person{name:"Negan",age:68,city:"救世堂"}
使用这种格式初始化时,需要注意:
- 必须初始化结构体的所有字段
- 初始值的填充循序必须与字段在结构体中的声明顺序一致
- 该方式不能和键值初始化方式混用
结构体内存布局
结构体占用一块连续的内存
package main
import "fmt"
func main() {
type test struct {
a int8
b int8
c int8
d int8
}
n := test{1,2,3,4}
fmt.Printf("n.a %p\n",&n.a) // n.a 0xc0000140a8
fmt.Printf("n.b %p\n",&n.b) // n.b 0xc0000140a9
fmt.Printf("n.c %p\n",&n.c) // n.c 0xc0000140aa
fmt.Printf("n.d %p\n",&n.d) // n.d 0xc0000140ab
}
空结构体
空结构体是不占内存的。
var v struct{}
fmt.Println(unsafe.Sizeof(v)) // 0
面试题
请问下面代码执行的结构是什么?
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name:"Negan",age: 68},
{name:"Alice",age:29},
{name:"小王八",age:10000},
}
for _,s := range stus{
m[s.name] = &s
}
fmt.Println(m)
for k,v := range m{
fmt.Println(k,"->",v.name)
}
}
输出结果
map[Alice:0xc0000044a0 Negan:0xc0000044a0 小王八:0xc0000044a0]
Negan -> 小王八
Alice -> 小王八
小王八 -> 小王八
说明:通过打印m我们可以知道,map的值都是同一个地址。所以导致所有的值都相同。for range在遍历切片的时候,创建了每个元素的副本,而不是直接返回每个元素的引用,如果使用该值变量的地址作为指向每个元素的指针,就会导致错误,在迭代时,返回的变量是迭代过程中根据切片依次赋值的新变量,所以值的地址总是相同。
type student struct {
name string
age int
}
func main() {
m := make(map[string]*student)
stus := []student{
{name:"Negan",age: 68},
{name:"Alice",age:29},
{name:"小王八",age:10000},
}
for _,s := range stus{
name := s
m[s.name] = &name
}
fmt.Println(m)
for k,v := range m{
fmt.Println(k,"->",v.name)
}
}
输出结果
map[Alice:0xc0000044c0 Negan:0xc0000044a0 小王八:0xc0000044e0]
小王八 -> 小王八
Negan -> Negan
Alice -> Alice
构造函数
Go语言的结构体没有构造函数,我们可以自己实现。
下方的代码实现了一个person 的构造函数,因为struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大。所以构造函数返回的是结构体的指针类型。
type person struct {
name string
age int8
city string
}
func newPerson(name,city string, age int8) *person{
return &person{
name:name,
age: age,
city:city,
}
}
func main() {
p := newPerson("Negan", "亚历山大",68) // 调用构造函数
fmt.Printf("%#v\n",p) // &main.person{name:"Negan", age:68, city:"亚历山大"}
}
方法和接收者
Go语言中的方法(Method) 是一种作用于特定类型变量的函数,这种特定类型变量叫做接收者(Receiver) 。接收者的概念就类似于其他语言中的this 或者self 。
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数){
函数体
}
- 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是
self 、this 之类的命名。例如Person 类型的接收者变量应该命名为p 。
- 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
- 方法名、参数列表、返回参数:具体格式与函数定义相同
// person结构体
type person struct {
name string
age int8
city string
}
// newPerson 构造函数
func newPerson(name,city string, age int8) *person{
return &person{
name:name,
age: age,
city:city,
}
}
// Dream person做梦的方法
func (p person) Dream(){
fmt.Printf("%s的梦想是学好Go语言\n",p.name)
}
func main() {
p := newPerson("Negan", "亚历山大",68)
fmt.Printf("%#v\n",p) // &main.person{name:"Negan", age:68, city:"亚历山大"}
p.Dream() //Negan的梦想是学好Go语言
}
方法与函数的区别是:函数不属于任何类型,方法属于特定的类型。
指针类型的接收者
指针类型的接收者由一个结构体的指针组成。由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this 或者self 。例如我们为person 添加一个setAge 方法。来修改实例变量的年龄。
// setAge 设置p的年龄
// 使用指针接收者
func (p *person) setAge(newAge int8){
p.age = newAge
}
调用该方法:
func main() {
p := newPerson("Negan", "亚历山大",68)
fmt.Println(p.age) // 68
p.setAge(30)
fmt.Println(p.age) // 30
值类型的接收者
当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作知识针对副本,无法修改接收者变量本身。
// setAge 设置p的年龄
// 使用值接收者
// setAge设置p的年龄,使用指针接收者
func (p person) setAge(newAge int8){
p.age = newAge
}
func main() {
p := newPerson("Negan", "亚历山大",68)
fmt.Println(p.age) // 68
p.setAge(30)
fmt.Println(p.age) // 68
}
什么时候应该使用指针类型接收者
- 需要修改接收者的值
- 接收者是拷贝代价比较大的大对象
- 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。
任意类型添加方法
在Go语言中,接收者的类型可以是任意类型,不仅仅是结构体,任何类型都可以拥有方法。
type MyInt int
func (m MyInt) sayHello(){
fmt.Println("hello,我是一个int")
}
func main() {
var m1 MyInt
m1.sayHello() // hello,我是一个int
m1 = 100
fmt.Printf("%#v %T\n",m1,m1) // 100 main.MyInt
}
注意事项:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。
结构体的匿名字段
结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。
// 结构体的匿名字段
// 结构体Person类型
type Person struct{
string
int
}
func main() {
p1 := Person{
"Negan",
68,
}
fmt.Printf("%#v\n", p1) // main.Person{string:"Negan", int:68}
fmt.Println(p1.string, p1.int) // Negan 68
}
匿名字段默认采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。
嵌套结构体
一个结构体中可以嵌套包含另一个结构体或者结构体指针。
// Address 地址结构体
type Address struct{
Province string
City string
}
// User 用户结构体
type User struct{
Name string
Gender string
Address Address
}
func main() {
user := User{
Name: "Negan",
Gender:"男",
Address: Address{
Province: "陕西",
City: "西安",
},
}
fmt.Printf("user=%#v\n", user)
// user=main.User{Name:"Negan", Gender:"男", Address:main.Address{Province:"陕西", City:"西安"}}
}
嵌套匿名结构体
// Address 地址结构体
type Address struct {
Province string
City string
}
// User 用户结构体
type User struct{
Name string
Gender string
Address // 匿名结构体
}
func main() {
var user User
user.Name = "Negan"
user.Gender = "男"
user.Address.Province = "陕西" // 通过匿名结构体.字段名访问
user.City = "西安" // 直接访问匿名结构体的字段名
fmt.Printf("user=%#v\n", user)
// user=main.User{Name:"Negan", Gender:"男", Address:main.Address{Province:"陕西", City:"西安"}}
}
当访问结构体成员时会现在结构体中查找该字段,找不到再去匿名结构体中查找。
嵌套结构体的字段名冲突
嵌套结构体内部可能存在相同的字段名,这个时候为了避免歧义需要制定具体的内嵌结构体的字段。
// Address 地址结构体
type Address struct{
Province string
City string
CreateTime string
}
// Email 邮箱结构体
type Email struct{
Account string
CreateTime string
}
// User 用户结构体
type User struct {
Name string
Gender string
Address
Email
}
func main() {
var user User
user.Name = "Negan"
user.Gender = "男"
user.Address.CreateTime = "2020"
user.Email.CreateTime = "2020"
fmt.Printf("%#v\n", user)
}
结构体的“继承”
Go语言中使用结构体可以实现其他编程语言中的面向对象继承。
// Animal 动物
type Animal struct{
name string
}
func (a *Animal) move(){
fmt.Printf("%s会动", a.name)
}
type Dog struct {
Feet int8
*Animal // 通过嵌套匿名结构体实现继承
}
func (d *Dog) wang(){
fmt.Printf("%s会汪汪汪~\n",d.name)
}
func main() {
d := &Dog{
Feet: 4,
Animal:&Animal{
name: "旺财",
},
}
d.wang() // 旺财会汪汪汪~
d.move() // 旺财会动
}
结构体字段的可见性
结构体中字段大写开头表示公开访问,小写表示私有(仅在定义当前结构体的包中可访问)
结构体与JSON序列化
JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。JSON 键值对是用来保存JS对象的一种方式,键/值对嘴和中的简明卸载前面并用双引号“” 包裹,使用: 分割,然后紧接着值,多个键值对之间使用, 分割。
// Student 学生
type Student struct {
ID int
Gender string
Name string
}
// Class 班级
type Class struct {
Title string
Student []*Student
}
func main() {
c := &Class{
Title: "101",
Student: make([]*Student,0,200),
}
for i:=0;i<10;i++{
stu := &Student{
Name: fmt.Sprintf("stu%02d",i),
Gender: "男",
ID:i,
}
c.Student = append(c.Student, stu)
}
// Json序列化:结构体--> Json格式的字符串
data, err := json.Marshal(c)
if err != nil{
fmt.Println("json marshal failed")
}
fmt.Printf("json:%s\n",data)
// Json反序列化:Json格式的字符串-->结构体
c1 := &Class{}
err = json.Unmarshal([]byte(data),c1)
if err != nil{
fmt.Println("json unmarshal failed!")
return
}
fmt.Printf("%#v\n", c1)
}
结构体标签(Tag)
Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag 在结构体字段的后方定义,由一对反引号“``”包裹起来。
`key1:"value1" key2:"value2"`
结构体tag由一个或多个键值对组成,键与值使用冒号分隔,值使用双引号括起来。同一个结构体字段可以这只多个键值对tag,不同的键值对之间使用空格分隔。
注意事项:为结构体编写Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误。通过反射也无法正确取值。例如不要在key和value之间添加空格。
// Student 学生
type Student struct {
ID int `json:"id"` // 通过指定tag实现json序列化该字段时的key
Gender string // json序列化默认使用字段名座位key
name string // 私有不能被json包访问
}
func main() {
s1 := Student{
ID:1,
Gender: "男",
name:"Negan",
}
data, err := json.Marshal(s1)
if err != nil{
fmt.Println("json marshal failed")
return
}
fmt.Printf("json str :%s\n",data) // json str :{"id":1,"Gender":"男"}
}
结构体和方法补充知识点
因为slice和map这两种数据类型都包含了指向底层数据的指针,因此在需要复制的时候特别注意
type Person struct{
name string
age int8
dreams []string // 切片
}
func (p *Person) SetDreams(dreams []string){
p.dreams = dreams
}
func main() {
p1 := Person{
name: "Negan",
age:68,
}
data := []string{"吃饭","睡觉","打豆豆"}
p1.SetDreams(data)
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]
data[1] = "不睡觉"
fmt.Println(p1.dreams) // [吃饭 不睡觉 打豆豆]
}
正确的做法是在方法中使用传入slice的拷贝进行结构体赋值。
type Person struct{
name string
age int8
dreams []string // 切片
}
func (p *Person) SetDreams(dreams []string){
p.dreams = make([]string,len(dreams))
copy(p.dreams,dreams)
}
func main() {
p1 := Person{
name: "Negan",
age:68,
}
data := []string{"吃饭","睡觉","打豆豆"}
p1.SetDreams(data)
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]
data[1] = "不睡觉"
fmt.Println(p1.dreams) // [吃饭 睡觉 打豆豆]
}
练习题
使用“面向对象”的思维方式编写一个学生信息管理系统
- 学生有id、姓名、年龄、分数等信息
- 程序提供展示学生列表,添加学生,编辑学生信息,删除学生等功能。
type student struct{
id int64
name string
}
// 造一个学生的管理者
type studentMgr struct{
allStudent map[int64]student
}
// 查看学生
func (s studentMgr) showStudents(){
// 从s.allStudent这个map中将所有的学生逐个拿出来
for _,stu := range s.allStudent{
fmt.Printf("学号:%d,姓名:%s\n",stu.id,stu.name)
}
}
// 增加学生
func (s studentMgr) addStudents(){
// 根据用户输入的内容创建一个新的学生
var (
stuId int64
stuName string
)
// 获取用户输入
fmt.Print("请输入学号")
fmt.Scanln(&stuId)
fmt.Print("请输入姓名")
fmt.Scanln(&stuName)
// 把新的学生放到s.allStudent这个map中
newStu := student{
id:stuId,
name:stuName
}
s.allStudent[newStu.id] = newStu
}
// 修改学生
func (s studentMgr) editStudents(){
// 获取用户输入学号
var stuId int64
fmt.Print("请输入要修改学生的学号")
fmt.Scanln(&stuId)
// 展示该学号对应的学生信息,如果没有则提示查无此人
stuObj, ok := s.allStudent[stuId]
if !ok{
fmt.Println("查无此人")
return
}
fmt.Printf("要修改的学生信息如下:学号:%d,姓名:%s",stuObj.id,stuObj.name)
fmt.Println("请输入学生新名字")
var newName string
fmt.Scanln(&newName)
// 更新学生的姓名
stuObj.name = newName
s.allStudent[stuId] = stuObj
}
// 删除学生
func (s studentMgr) deleteStudents(){
// 请用户输入要删除学生的id
var stuId int64
fmt.Println("请输入要删除学生的学号")
fmt.Scanln(&stuId)
// 在map中查找这个学生
_,ok := s.allStudent[stuId]
if !ok{
fmt.Println("查无此人")
return
}
// 删除,如何从map中删除键值对
delete(s.allStudent, stuId)
fmt.Println("删除成功")
}
var smr studentMgr // 声明一个全局变量学生管理对象smr
// 菜单函数
func showMenu(){
fmt.Println("welcome sms")
fmt.Println(`
1、查看所有学生
2、添加学生
3、修改学生
4、删除学生
5、退出
`)
}
func main(){
smr = studentMgr{
allStudent:make(map[int64]student,100)
}
for{
showMenu() // 给用户展示菜单
// 等待用户输入
fmt.Print("请输入菜单序号:")
var choice int
fmt.Scanln(&choice)
fmt.Println("您输入的是:",choice)
switch choice{
case 1:
smr.showStudents()
case 2:
smr.addStudents()
case 3:
smr.editStudents()
case 4:
smr.deleteStudents()
case 5:
os.Exit(1) // 退出
default:
fmt.Println("请滚")
}
}
}
本文参考:https://www.liwenzhou.com/posts/Go/10_struct/
|
请发表评论