golang进阶之构造函数、方法、结构体进阶

BigSun丶 / 2024-03-06 / 原文

目录
  • 一、构造函数
  • 二、方法和接收者
    • 1. 方法的定义格式
    • 2. 指针类型的接收者
    • 3. 值类型的接收者
    • 4. 指针接收者和值接收者的使用场景
    • 5. 非结构体的接收者
  • 三、结构体进阶
    • 1. 结构体的匿名字段
    • 2. 嵌套结构体
    • 3. 嵌套匿名字段
    • 4. 嵌套结构体的字段名冲突
    • 5. 结构体的“继承”
    • 6. 结构体字段的可见性
    • 7. 结构体与JSON序列化
    • 8. 结构体标签(Tag)
    • 9. 结构体和方法补充

一、构造函数

  • Go语言的结构体没有构造函数,但我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型
func newPerson(name, city string, age int8) *person {
	return &person{
		name: name,
		city: city,
		age:  age,
	}
}

p9 := newPerson("张三", "虹桥", 90)
fmt.Printf("%#v\n", p9)  // &main.person{name:"张三", city:"虹桥", age:90}

二、方法和接收者

  • Go语言中的 方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self 的功能(即一个变量点一个方法,看似实现了面向对象的功能)
  • 在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法

1. 方法的定义格式

  • 方法与函数的区别是,函数不属于任何类型,方法属于特定的类型

  • 定义格式:
    func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
        函数体
    }
    
    其中,
    
    - 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等
    
    - 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型
    
    - 方法名、参数列表、返回参数的定义规则与函数的函数名、参数、返回值的定义相同
    
    
    示例:
    //Person 结构体
    type Person struct {
    	name string
    	age  int8
    }
     
    //NewPerson 构造函数
    func NewPerson(name string, age int8) *Person {
    	return &Person{
    		name: name,
    		age:  age,
    	}
    }
     
    //Dream Person做梦的方法
    func (p Person) Dream() {
    	fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
    }
     
    func main() {
    	p1 := NewPerson("炮王", 25)
    	p1.Dream()
    }
    

2. 指针类型的接收者

  • 相当于为指针类型添加方法

  • 指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄

//Person 结构体
type Person struct {
	name string
	age  int8
}
 
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

// 定义 SetAge 方法来设置p的年龄
// 使用指针接收者
func (p *Person) SetAge(newAge int8) {
	p.age = newAge
}

func main() {
	p1 := NewPerson("炮王", 25)
	fmt.Println(p1.age) // 25
	p1.SetAge(30)  // 相当于 (*p1).SetAge(30) 。(Go语法糖)需要注意的是在Go语言中支持对结构体指针直接使用 `.` 来访问结构体的成员,进行修改和赋值操作。
	fmt.Println(p1.age) // 30
}

3. 值类型的接收者

  • 相当于为值类型添加方法

  • 当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份副本。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

//Person 结构体
type Person struct {
	name string
	age  int8
}
 
//NewPerson 构造函数
func NewPerson(name string, age int8) *Person {
	return &Person{
		name: name,
		age:  age,
	}
}

// 定义 SetAge2 方法来设置p的年龄
// 使用值接收者
func (p Person) SetAge2(newAge int8) {
	p.age = newAge
}
 
func main() {
	p1 := NewPerson("炮王", 25)
	fmt.Println(p1.age) // 25
	p1.SetAge2(30)  // 相当于 (*p1).SetAge2(30) 。(Go语法糖)需要注意的是在Go语言中支持对结构体指针直接使用 `.` 来访问结构体的成员,进行修改和赋值操作。
	fmt.Println(p1.age) // 25 ,上面一行的 p1.SetAge2(30) 修改的是 p1的副本,并不是结构体p1本身
}

4. 指针接收者和值接收者的使用场景

  1. 需要修改接收者中的值
  2. 接收者是拷贝代价比较大的大对象
  3. 保证一致性:如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者(即对于同一个接收者的方法们来说,要么都用指针接收者,要么都用值接收者)

5. 非结构体的接收者

  • 在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法

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

  • 相当于为自定义的类型添加方法

//MyInt 将int定义为自定义MyInt类型
type MyInt int
 
//SayHello 为MyInt添加一个SayHello的方法
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
}

三、结构体进阶

1. 结构体的匿名字段

  • 结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段
  • 注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个
//Person 结构体Person类型
type Person struct {
	string
	int
}
 
func main() {
	p1 := Person{
		"炮王",
		18,
	}
	fmt.Printf("%#v\n", p1)        // main.Person{string:"炮王", int:18}
	fmt.Println(p1.string, p1.int) // 炮王 18
    
    var p2 Person
    p2.string = "山东"    // 匿名字段默认使用类型名作为字段名
    p1.int = 5    // 匿名字段默认使用类型名作为字段名
    fmt.Printf("%#v\n", p2)  // main.Person{string:"山东", int:5}
	fmt.Println(p2.string,p2.int)  // 山东 5 
}

2. 嵌套结构体

  • 一个结构体中可以嵌套包含另一个结构体或结构体指针,就像下面的示例代码那样(即类似python中的字典套字典)
//Address 地址结构体
type Address struct {
	Province string
	City     string
}
 
//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address Address
}
 
func main() {
	user1 := User{
		Name:   "炮王",
		Gender: "女",
		Address: Address{
			Province: "山东",
			City:     "威海",
		},
	}
	fmt.Printf("user1=%#v\n", user1)  // user1=main.User{Name:"炮王", Gender:"女", Address:main.Address{Province:"山东", City:"威海"}}
}

3. 嵌套匿名字段

  • 当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找

  • 上面user结构体中嵌套的Address结构体也可以采用匿名字段的方式,例如

//Address 地址结构体
type Address struct {
	Province string
	City     string
}
 
//User 用户结构体
type User struct {
	Name    string
	Gender  string
	Address  // 匿名字段
}
 
func main() {
	var user2 User
	user2.Name = "炮王"
	user2.Gender = "女"
	user2.Address.Province = "山东"    // 匿名字段默认使用类型名作为字段名
	user2.City = "威海"                // 匿名字段可以省略,等同于user2.Address.City = "威海" ,当访问 City 时会先在结构体中查找该字段,找不到再去嵌套的匿名字段Address中查找City
	fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"炮王", Gender:"女", Address:main.Address{Province:"山东", City:"威海"}}
}

4. 嵌套结构体的字段名冲突

  • 多个嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名
//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 user3 User
	user3.Name = "虹桥阿香"
	user3.Gender = "女"
	// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime
	user3.Address.CreateTime = "2000" //指定Address结构体中的CreateTime
	user3.Email.CreateTime = "2000"   //指定Email结构体中的CreateTime
}

5. 结构体的“继承”

  • Go语言中使用结构体也可以实现其他编程语言中面向对象的继承
//Animal 动物
type Animal struct {
	name string
}
 
func (a *Animal) move() {
	fmt.Printf("%s会动!\n", a.name)
}
 
//Dog 狗
type Dog struct {
	Feet    int8
	*Animal //通过嵌套匿名结构体实现继承
}
 
func (d *Dog) wang() {
	fmt.Printf("%s会汪汪汪~\n", d.name)
}
 
func main() {
	d1 := &Dog{
		Feet: 4,
		Animal: &Animal{ //注意嵌套的是结构体指针
			name: "乐乐",
		},
	}
	d1.wang() //乐乐会汪汪汪~
	d1.move() //乐乐会动!
}

6. 结构体字段的可见性

  • 在之前讲Go语言包的时候,就说过,Go语言按照首字母大小写来开启关闭其他包对其的访问权限的。所以结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

7. 结构体与JSON序列化

  • 就是结构体和json数据之间的转换
  • 使用内置库 encoding/json
    • json序列化:json.Marshal()
    • json反序列化:json.Unmarshal()
package main

import (
	"encoding/json"
	"fmt"
)

// Student 学生
type Student struct {
	ID     int
	Gender string
	Name   string
}

// Class 班级
type Class struct {
	Title    string
	Students []*Student
}

func main() {
	c := &Class{
		Title:    "101",
		Students: make([]*Student, 0, 200),
	}
	for i := 0; i < 10; i++ {
		stu := &Student{
			Name:   fmt.Sprintf("stu%02d", i),
			Gender: "女",
			ID:     i,
		}
		c.Students = append(c.Students, stu)
	}
	//JSON序列化:结构体-->JSON格式的字符串
	data, err := json.Marshal(c)
	if err != nil {
		fmt.Println("json marshal failed")
		return
	}
	fmt.Printf("json:%s\n", data)
	//JSON反序列化:JSON格式的字符串-->结构体
	str := `{"Title":"101","Students":[{"ID":0,"Gender":"女","Name":"stu00"},{"ID":1,"Gender":"女","Name":"stu01"},{"ID":2,"Gender":"女","Name":"stu02"},{"ID":3,"Gender":"女","Name":"stu03"},{"ID":4,"Gender":"女","Name":"stu04"},{"ID":5,"Gender":"女","Name":"stu05"},{"ID":6,"Gender":"女","Name":"stu06"},{"ID":7,"Gender":"女","Name":"stu07"},{"ID":8,"Gender":"女","Name":"stu08"},{"ID":9,"Gender":"女","Name":"stu09"}]}`
	c1 := &Class{}
	err = json.Unmarshal([]byte(str), c1)
	if err != nil {
		fmt.Println("json unmarshal failed!")
		return
	}
	fmt.Printf("%#v\n", c1)
}

8. 结构体标签(Tag)

  • Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。 Tag 在结构体字段的后方定义,由一对反引号包裹起来,具体的格式如下

    • `key1:"value1" key2:"value2"`
      
  • 结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔

  • 注意事项: 为结构体编写 Tag 时,必须严格遵守键值对的规则(不要随意添加空格和换行)。因为结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格

- 示例:为Student结构体的每个字段定义json序列化时使用的Tag

//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:   "虹桥阿香",
	}
	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":"女"}
}

9. 结构体和方法补充

  • 因为 slice(切片)和map 这两种数据类型都包含了指向底层数据的指针(即他们都是引用类型的数据),因此我们在结构体或者方法中需要修改它们时,要特别注意
  • 同样的问题也存在于返回值是 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: "炮王", age: 18}
	data := []string{"吃饭", "睡觉", "打豆豆"}
	p1.SetDreams(data)
 
	// 你真的想要修改 p1.dreams 吗?
	data[1] = "不睡觉"
	fmt.Println(p1.dreams)  // [吃饭 不睡觉 打豆豆]  ,可以看到p1中的dreams也变成了 不睡觉

}

**********************正确的做法是在方法中使用传入的slice的拷贝来进行结构体赋值**********************
func (p *Person) SetDreams(dreams []string) {
	p.dreams = make([]string, len(dreams))
	copy(p.dreams, dreams)  // 将传入的原切片的值进行拷贝给 p.dream ,这样上面最后打印的p1.dreams就变成了:[吃饭 睡觉 打豆豆] 
}