zoukankan      html  css  js  c++  java
  • Go中的结构体

    本文参考:https://www.liwenzhou.com/posts/Go/10_struct/

    结构体

    Go语言中的基本数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或者部分属性时,这时候再用单一的基本数据类型明显就无法满足需求。Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,struct

    Go语言中通过struct来实现面向对象。

    结构体的定义

    使用typestruct关键字来定义结构体

    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
    ",p)  // p={Negan 西安 18}
        fmt.Printf("p=%#v
    ",p)  // p=main.person{name:"Negan",age:18,city:"西安"}
    }
    

    我们通过.来访问结构体字段(成员变量),例如p.namep.age等。

    匿名结构体

    在定义一些临时数据结构等常见下可以使用匿名结构体

    func main(){
    	var user struct{name string;age int}
    	user.name = "Negan"
    	user.age = 18
    	fmt.Printf("%#v
    ", user)
    }
    

    创建指针类型结构体

    通过使用new关键字对结构体进行实例化,得到的是结构体地址。

    var p = new(person)
    fmt.Printf("%T
    ",p)  // *main.person
    fmt.Printf("p=%#v
    ",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
    ",p)  // p=&main.person{name:"Negan",age:68,city:"亚历山大"}
    

    取结构体的地址实例化

    使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

    p := &person{}
    fmt.Printf("%T
    ",p)  // *main.person
    fmt.Printf("p=%v
    ",p)  // p=&main.person{name:"",age:0,city:""}
    p.name = "Negan"
    p.age = 68
    p.city = "救世堂"  
    fmt.Printf("p=%#v
    ",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
    ",p)  // p=main.person{name:“”,age:0,city:""}
    }
    

    使用键值对初始化

    使用键值对对结构体进行初始化,键对应结构体的字段,值对应该字段的初始值。

    p := person{
    	name:"Negan",
    	age:68,
    	city:"亚历山大"
    }
    fmt.Printf("p=%#v
    ",p)  // p=main.person{name:"Negan",age:68,city:"亚历山大"}
    

    也可以使用结构体指针进行键值对初始化

    p := &person{
    	name:"Negan",
    	age:68,
    	city:"亚历山大"
    }
    fmt.Printf("p=%#v
    ",p)  //p=&main.person{name:"Negan",age:68,city:"亚历山大"}
    

    当某些字段没有初始值的时候,该字段可以不写,此时没有指定初始值的字段的值就是该字段类型的零值。

    p := &person{
        city:"救世堂"
    }
    fmt.Printf("p=%#v
    ",p)  // p=&main.person{name:"",age:0,city:"救世堂"}
    

    使用值的列表初始化

    初始化结构体的时候可以简写,也就是初始化的时候不写键,直接写值

    p := &person{
    	"Negan",
    	68,
    	"救世堂"
    }
    fmt.Printf("p=%#v
    ",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.a)  // n.a 0xc0000140a8
    	fmt.Printf("n.b %p
    ",&n.b)  // n.b 0xc0000140a9
    	fmt.Printf("n.c %p
    ",&n.c)  // n.c 0xc0000140aa
    	fmt.Printf("n.d %p
    ",&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
    ",p)  // &main.person{name:"Negan", age:68, city:"亚历山大"}
    
    }
    

    方法和接收者

    Go语言中的方法(Method)是一种作用于特定类型变量的函数,这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者self

    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数){
    	函数体
    }
    
    • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如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语言
    ",p.name)
    }
    
    func main() {
    	p := newPerson("Negan", "亚历山大",68)
    	fmt.Printf("%#v
    ",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
    ",m1,m1)  // 100 main.MyInt
    }
    

    注意事项:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法。

    结构体的匿名字段

    结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段。

    // 结构体的匿名字段
    // 结构体Person类型
    type Person struct{
    	string
    	int
    }
    
    func main() {
    	p1 := Person{
    		"Negan",
    		68,
    	}
    	fmt.Printf("%#v
    ", 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
    ", 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
    ", 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
    ", 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会汪汪汪~
    ",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
    ",data)
    
    	// Json反序列化:Json格式的字符串-->结构体
    	c1 := &Class{}
    	err = json.Unmarshal([]byte(data),c1)
    	if err != nil{
    		fmt.Println("json unmarshal failed!")
    		return
    	}
    	fmt.Printf("%#v
    ", 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
    ",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
    ",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/

  • 相关阅读:
    借贷宝什么鬼 砸钱推广是妙招还是险棋
    div+css 怎么让一个小div在另一个大div里面 垂直居中
    php重新整理数组索引
    JS 得细心的坑位
    chrome表单自动填充导致input文本框背景变成偏黄色问题解决
    phpstorm配置xdebug
    MySQLi基于面向对象的编程
    PHP中开启gzip压缩的2种方法
    SVN创建主干,分支、合并分支
    懒加载和预加载【转载】
  • 原文地址:https://www.cnblogs.com/huiyichanmian/p/12787788.html
Copyright © 2011-2022 走看看