12 结构体

  • Go 中无 ·类· 的概念,不支持类的继承等面向对象的概念
  • 通过 结构体的内嵌再配合接口 比面向对象具有更高的扩展性和灵活性

类型别名和自定义类型

自定义类型

  • Go 中的基本数据类型:string, int… ,浮点数
  • Go 可以使用type 关键字定义自定义类型
  • 自定义类型:定义了一个全新的类型
    • 可以基于内置基本数据类型
    • 可以通过struct定义
  • type name T
  • name 就是一种新类型,具有 T类型的特性

类型别名

  • 类型别名规定:type TypeAlias = Type
    • TypeAlias 只是Type别名
    • 本质上是同一个类型
  • runebyte 就是类型别名:
    • type rune = int32
    • type byte = uint8

类型定义 和 类型别名的区别

  • type NewInt int : %T : main.NewInt
  • type MyInt = int: %T : int

结果显示a的类型是main.NewInt,表示main包下定义NewInt类型。

b的类型是intMyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。

结构体

  • 基本数据类型可以表示一些事物的部分属性
  • 结构体可以封装多个基本数据类型,表达一个事物的全部属性
    • 本质上是一种聚合类型
  • Go 通过struct实现面向对象

结构体的定义

  • 使用typestruct

    • ```go
      type 类型名 struct{
      字段名 字段类型
      字段名 字段类型
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31

      * 类型名:同一个包内不能重复

      * 字段名:字段名必须唯一,同样字段类型的字段名可写在同一行

      * 字段类型:结构体字段的具体类型

      ### 结构体实例化

      * 只有当结构体实例化时,才会真正的分配内存,才能使用结构体的字段
      * 使用关键字`var`声明:`var 结构体实例 结构体类型`

      #### 基本实例化

      * 声明后,使用操作符`.`访问结构体字段(成员变量)进行初始化

      * ```go
      type person struct {
      name string
      city string
      age int8
      }

      func main() {
      var p1 person
      p1.name = "沙河娜扎"
      p1.city = "北京"
      p1.age = 18
      fmt.Printf("p1=%v\n", p1) //p1={沙河娜扎 北京 18}
      fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"沙河娜扎", city:"北京", age:18}
      }

匿名结构体

  • 定义一些临时数据结构,使用匿名结构体

    var user struct{Name string; Age int}

创建指针类型结构体

  • 可以使用new 对结构体进行实例化,得到结构体的地址

    1
    2
    3
    var p2 = new(person)
    fmt.Printf("%T",p2) //*main.person
    fmt.Printf("%#v",p2) //&main.person(..)
  • p2 是一个结构体指针

  • 注意:Go 支持对结构体指针直接使用.访问结构体成员

取结构体的地址实例化

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

  • ```go
    p3 := &person()
    p3.name = “qimi”

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    * `p3.name = "qimi"` 在底层 是`(*p3).name = "qimi"`

    * 是 Go 帮我们实现的语法糖



    ### 结构体初始化

    * **未初始化的结构体**,其成员变量都是对应其类型的零值

    * ```go
    type person struct {
    name string
    city string
    age int8
    }

    func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
    }

使用键值对初始化

  • 键对应结构体的字段

  • 值对应该字段的初始值

    • ```go
      p5 := person{
      name : "小王子",
      city : "beijing",
      age : 18,
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9

      * 对结构体指针进行键值对初始化:

      * ```go
      p6 := &person{
      name: "小王子",
      city: "北京",
      age: 18,
      }
  • 未指定初始值的字段的值为该字段类型的零值

    • ```go
      p7 := &person{

      city: "北京",
      

      }

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13



      #### 使用值得列表初始化

      * 初始化结构体时,直接写值:

      * ```go
      p8 := &person{
      "沙河娜扎",
      "北京",
      28,
      }
    • 必须要初始化结构体的所有字段

    • 初始值的填充顺序必须与字段,在结构体中的声明顺序一致

    • 不能和键值对初始化混用

结构体内存布局

  • 结构体占用一块 连续的内存

空结构体

  • 空结构体不占用空间

fmt.Printf(unsafe.Sizeof(v)) //0

面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type student struct {
name string
age int
}

func main() {
m := make(map[string]*student)
stus := []student{
{name: "小王子", age: 18},
{name: "娜扎", age: 23},
{name: "大王八", age: 9000},
}

for _, stu := range stus {
m[stu.name] = &stu //m : "小王子":&student{name: "小王子", age: 18}
}
for k, v := range m {
fmt.Println(k, "=>", v.name)
}
}
  • 执行结果:

    1
    2
    3
    小王子 => 小王子
    娜扎 => 娜扎
    大王八 => 大王八
  • 运行结果:

    1
    2
    3
    大王八 => 大王八
    小王子 => 大王八
    娜扎 => 大王八

构造函数

  • Go 结构体没有构造函数,需要自己实现

  • struct 是值类型,值拷贝性能开销比较大,所以构造函数返回的是结构体指针类型

    • ```go
      func newPerson(name, city string, age int8) *person {
      return &person{
          name: name,
          city: city,
          age:  age,
      }
      
      }
      1
      2
      3
      4
      5

      * 调用:

      * ```go
      p9 := newPerson("张三", "沙河", 90)

方法和接收者

  • Go 中的方法(Method):一种作用于特定类型变量的函数

    • 这种特定类型变量叫做接收者Receiver
    • 接收者的概念类似于this, self
  • 方法的定义格式

    • ```go
      func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
      函数体
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
        * **接收者变量:**接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是`self`、`this`之类的命名
      * **接收者类型:**接收者类型和参数类似,可以是**指针类型和非指针类型**
      * **方法名、参数列表、返回参数:** 与函数定义相同

      * 示例:

      ```go
      //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()
      }
  • 方法与函数的区别:

    • 函数不属于任何类型
    • 方法属于特定类型

指针类型的接收者

  • 指针类型的接收者 : 结构体的指针

  • 调用方法时,修改接收者指针的任意成员变量,都是有效的

    • 接近于其他面向对象中的this, self
  • 示例:一个方法:修改实例变量的年龄

    • ```go
      // SetAge 设置p的年龄
      // 使用指针接收者
      func (p *Person) SetAge(newAge int8) {
      p.age = newAge
      
      }
      1
      2
      3
      4
      5
      6
      7
      8

      * ```go
      func main() {
      p1 := NewPerson("小王子", 25)
      fmt.Println(p1.age) // 25
      p1.SetAge(30)
      fmt.Println(p1.age) // 30
      }

值类型的接收者

  • 方法作用于 值类型接收者时,Go 会在运行前将接收者的值复制一份

  • 在值类型接收者的方法中,可以获取接收者的成员值,但无法修改接收者变量本身

    • ```go
      // SetAge2 设置p的年龄
      // 使用值接收者
      func (p Person) SetAge2(newAge int8) {
      p.age = newAge
      
      }func main() {
      p1 := NewPerson("小王子", 25)
      p1.Dream()
      fmt.Println(p1.age) // 25
      p1.SetAge2(30) // (*p1).SetAge2(30)
      fmt.Println(p1.age) // 25
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29

      #### 何时使用 指针类型的接收者

      * 需要修改接收者的成员值
      * 接收者是 拷贝代价比较大的大对象
      * 保证一致性,若有个方法使用了指针接收者,其他也应该同步



      ### 任意类型添加方法

      * 在Go中,**任何类型**都可以拥有方法,**接收者可以是任何类型**

      * 例如:将内置类型`int` 使用关键字type,定义为新的自定义类型,可以为其添加方法

      * ```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
      }
  • 注意:不能给其他包的 类型定义方法

结构体的匿名字段

  • 结构体 允许 其成员字段在声明时,没有字段名,只有类型

  • ==注意:==Go 会默认采用类型名作为字段名,结构体要求字段名称必须唯一

    • 因此,一个结构体中,同种类型的匿名字段只能有一个
  • ```go
    //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
    

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34

    *

    ### 嵌套结构体

    * 结构体中,可以**嵌套包含** 另一个结构体或结构体指针

    * 示例

    ```go
    //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:"威海"}}
    }

嵌套匿名字段

  • 嵌套的结构体也可采用匿名字段:

    • ```go
      //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 = "威海"                // 匿名字段可以省略
      fmt.Printf("user2=%#v\n", user2) //user2=main.User{Name:"小王子", Gender:"男", Address:main.Address{Province:"山东", City:"威海"}}
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39

      * 当访问结构体成员时,现在结构体中查找该字段,再去嵌套的匿名字段找



      #### 嵌套结构体的字段名冲突

      * 嵌套结构体内部可能存在相同的字段名。在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名。

      ```go
      //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
      }

结构体的 “继承”

  • Go 中使用结构体,可以实现其他面向对象中的 继承

    • ```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() //乐乐会动!
      
      }
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67



      ### 结构体字段的可见性

      * 结构体中:
      * **大写**开头表示**可公开访问**
      * **小写**表示**私有**(仅在定义当前结构体的包中访问)



      ### 结构体 与 JSON 序列化

      * `JSON` :`JavaScript Object Notation`

      * 轻量级的**数据交换格式**
      * 易于 阅读和编写
      * 易于 机器解析和生成

      * JSON 键值对:用于保存 JS 对象的一种方式

      * 键/值对 格式:`"键名" : 值, `

      * ```go
      //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)
      }

结构体标签(Tag)

  • Tag 是结构体中的 元信息,运行的时候通过反射机制读取出来

    • Tag结构体字段的后方定义,用==反引号==包裹

    • ```
      key1:"balue1" key2:"value2"

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33

      * 结构体`tag` 有一个或多个键值对组成

      * 键与值是用冒号分隔,值使用双引号括起来
      * 同一个结构体字段可以设置多个键值对,不同键值对使用空格分隔

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



      * 示例:每个字段定义json序列化时使用的 Tag

      * ```go
      //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":"男"}
      }

结构体和方法补充知识点

  • slice 和 map都包含了指向底层数据的指针,在复制时需要特别注意

    • ```go
      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)  // ?
      

      }

      1
      2
      3
      4
      5
      6
      7
      8

      * 正确的做法:在方法中使用传入的 slice 的拷贝,进行结构体赋值

      * ```go
      func (p *Person) SetDreams(dreams []string) {
      p.dreams = make([]string, len(dreams))
      copy(p.dreams, dreams)
      }
    • 同样的问题:返回值 slice 和 map情况