go语言的结构体

go语言的结构体

目录

简介

go语言中没有类的概念,也不支持类的继承等面向对象的概念, 但在结构体的内嵌配合接口比面向对象更具更高的扩展性和灵活性。

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

定义

使用typestruct关键字来定义结构体,格式如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

其中:

  • 类型名:表示自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名,结构体中的名字必须唯一。
  • 字段类型:表示结构体字段的具体类型。

举个例子,定义一个坐标结构体:

type Point struct {
    X int
    Y int
}

同种类型的字段也可以放一行,定义一个颜色的三原色结构体:

type Color struct {
    R, G, B byte
}

结构体的实例化

结构体的定义只是一种内存布局的描述,此时并不会分配内存,只有实例化的时候才会真正分配内存。

在定义结构体并实例化才能使用结构体字段,有多重实例方式。

基本实例化

结构体本身是一种类型,可以像声明变量一样,以 var 的方式声明结构体即可完成实例化。

var 结构体实例 结构体类型

举个例子:

package main

import (
        "fmt"
)

type Person struct {
        name string
        city string
        age  int
}

func main() {
        var p1 Person
        p1.name = "江子牙"
        p1.city = "江西"
        p1.age = 23

        // {江子牙 江西 23}
        fmt.Println(p1)
        
        // 类型:main.Person       值:main.Person{name:"江子牙", city:"江西", age:23}
        fmt.Printf("类型:%T\t值:%#v", p1, p1)
}

我们可以通过.来对结构体的字段进行赋值和取值操作。

创建指针类型的结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

package main

import (
        "fmt"
)

type Person struct {
        name string
        city string
        age  int
}

func main() {
        p1 := new(Person)

        // &{  0} 得到的是一个结构体指针,字段的值对应该字段类型的零值
        fmt.Println(p1)
        // 指针:0xc00006e2d0      类型:*main.Person      值:&main.Person{name:"", city:"", age:0}
        fmt.Printf("指针:%p\t类型:%T\t值:%#v", p1, p1, p1)
}

尽管得到的是一个结构体指针,但是go语言中同样可以通过.来进行对字段进行操作:

package main

import (
        "fmt"
)

type Person struct {
        name string
        city string
        age  int
}

func main() {
        p1 := new(Person)

        p1.name = "江子牙"
        p1.city = "江西"
        p1.age = 23

        // &{江子牙 江西 23}
        fmt.Println(p1)
        // 指针:0xc00006e2d0      类型:*main.Person      值:&main.Person{name:"江子牙", city:"江西", age:23}
        fmt.Printf("指针:%p\t类型:%T\t值:%#v", p1, p1, p1)
}

取结构体的地址实例化

使用&对结构体进行取地址操作相当于对结构体进行了一次new操作。

package main

import (
        "fmt"
)

type Person struct {
        name string
        city string
        age  int
}

func main() {
        p2 := &Person{}
        p2.name = "江子牙"
        p2.city = "江西"
        p2.age = 23

        // &{江子牙 江西 23}
        fmt.Println(p2)
        // 指针:0xc00006e2d0      类型:*main.Person      值:&main.Person{name:"江子牙", city:"江西", age:23}
        fmt.Printf("指针:%p\t类型:%T\t值:%#v", p2, p2, p2)
}

结构体的初始化

键值对初始化

类似于Python的关键字传参

package main

import (
        "fmt"
)

type Person struct {
        name string
        age int
        isLogin bool
} 

func main() {

        p1 := Person{
                name:"江子牙",
                age:23,
                isLogin:true,
        }
        
        // {江子牙 23 true}
        fmt.Println(p1)
        
}

值的列表初始化

类似于Python的位置参数

package main

import (
        "fmt"
)

type Person struct {
        name string
        age int
        isLogin bool
}

func main() {

        p2 := Person{
                "江子牙", 23, true,
        }
        // {江子牙 23 true}
        fmt.Println(p2)
}

使用此方式初始化时,值得注意的是:

  • 必须初始化结构体的所有字段
  • 初始化顺序必须和结构体声明时保持一致
  • 两种方式的初始化不能混用

匿名结构体的初始化

顾名思义,匿名结构体就是没有名字的结构体,无需用type关键字声明就可以直接使用。

package main

import (
        "fmt"
)

func main() {
        s1 := struct {
                name string
                age  int
        }{
                "江子牙",
                23,
        }
    
        // 江子牙
        fmt.Println(s1.name)
        // 23
        fmt.Println(s1.age)
        // {江子牙 23}
        fmt.Println(s1)
}

构造函数

结构体没有构造函数,我们可以自己实现。因为结构体是值类型,如果结构体较复杂的话,值拷贝的性能开销会比较大,所以我们的构造函数返回结构体指针类型。

package main

import "fmt"

type Cat struct {
        Name  string
        Color string
}

func NewCat(name, color string) *Cat {
        return &Cat{
                Name:  name,
                Color: color,
        }
}

func main() {
        
    // 调用构造函数实例化一个小豹子
        cat := NewCat("小豹子", "豹纹")
        // &{小豹子 豹纹}
        fmt.Println(cat)
        // 小豹子
        fmt.Println(cat.Name)
        // 豹纹
        fmt.Println(cat.Color)
}

方法Method

go语言中的方法method是一种作用于特定类型变量的函数,叫做receiver,可以理解为其他语言的thisself

格式如下:

func (接收器变量 接收器类型) 方法名 (参数列表) (返回值列表) {
    函数体
}
  • 接收器变量:官方建议,该接收器变量最好和接收器类型的第一个字母保持一致。如 Cat c 、Bag b 、Dog d。
  • 接收者类型:可以是指针类型和非指针类型。
  • 方法名、参数列表、返回值:与函数定义相同。
package main

import "fmt"

// 定义一个包的结构体
type Bag struct {
        items []interface{}
}

// 为结构体定义一个Insert方法
func (b *Bag) Insert(thing interface{}) {
        b.items = append(b.items, thing)
}

func main() {

        // 实例化一个包
        bag := new(Bag)
        // 放入一个东西
        bag.Insert("口红")

        // [口红]
        fmt.Println(bag.items)
        // &{[口红]}
        fmt.Println(bag)


        // 放入一个手机
        bag.Insert("手机")
        // [口红 手机]
        fmt.Println(bag.items)
        // &{[口红 手机]}
        fmt.Println(bag)

}

接收者

指针类型的接收者

指针类型的接收器是一个结构体的指针,更接近于面向对象的this or self。

由于指针的特性,调用方法时,修改接收器指针的任意成员变量,在方法调用结束后,已经修改过得都是有效的。

上个例子的接收器*Bag就是一个指针接收器,所以才能append之后保持修改。

值类型的接收者

当方法作用与非指针接收器时,go语言内部会在代码运行的时候将接收器的数据复制一份,在非指针接收器的方法可以获取,也可以修改,只是修改无效而已。

package main

import "fmt"

type Bag struct {
        name string
}

func (b *Bag) SetName(name string) {
        b.name = name
}

func (b Bag) SetName1(name string) {
        fmt.Println(name, "修改不生效")
        b.name = name
}

func main() {

        // 实例化一个包
        bag := new(Bag)

        // 设置
        bag.SetName("指针类型的接收者")
        // 生效了:指针类型的接收者
        fmt.Println(bag.name)
        // 再设置
        bag.SetName1("值类型的接收者")
        // 不生效:指针类型的接收者
        fmt.Println(bag.name)

}

总结

  • 需要修改接收者中的值

  • 接收者是拷贝代价比较大的大对象

  • 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

为任意类型添加方法

go语言可以对任何一种类型添加方法。给一种类型添加方法就像给结构体添加方法一样。

package main

import "fmt"

// 定义一个MyInt类型
type MyInt int

// 为 MyInt 添加是否为0的方法
func (m MyInt) IsZero() bool {
        return m == 0
}

// 为 MyInt 添加add()方法
func (m MyInt) Add(other int) int {
        return int(m) + other
}

func main() {
        var b MyInt
        // 定义之后为零值
        fmt.Println(b.IsZero())
        
        b = 100
        // 赋值之后不为0
        fmt.Println(b.IsZero())
        // 两数相加:100+150
        fmt.Println(b.Add(150))
}

结构体的匿名字段

结构体允许字段没有字段名,只有类型。这种字段就叫做匿名字段。

package main

import "fmt"

// 定义一个匿名字段的结构体
type Person struct {
        string
        int
}

func main() {
        p1 := &Person{
                "江子牙",
                23,
        }
        // &{江子牙 23}
        fmt.Println(p1)
}

嵌套结构体

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

比如:

package main

import "fmt"

// 定义一个学生的结构体
type Student struct {
        name string
        age int
        *Class  // 嵌套一个匿名结构体的指针
        School // 嵌套一个匿名结构体
}

// 定义一个班级结构体
type Class struct {
        className string
}

// 定义一个学校结构体
type School struct {
        schoolName string
}

func main() {
        s1 := &Student{
                "江子牙",
                23,
                &Class{
                        "高三一班",
                },
                School{"明珠中学"},
        }
        // &{江子牙 23 0xc0000421c0 {明珠中学}}
        fmt.Println(s1)
        // 高三一班
        fmt.Println(s1.className)
        // 明珠中学
        fmt.Println(s1.schoolName)
}

学生结构体嵌套了班级和学校两个结构体,获取班级学校和班级名字的时候,会一直往下找。可以通过.的方式。

结构体的继承

go语言没有面向对象,更没有继承。只是可以实现同样的效果。

package main

import "fmt"

// 定义一个儿子的结构体
type Child struct {
        name string
        *Father // 通过匿名结构体实现继承
}

// 定义一个父亲结构体
type Father struct {
        money int64
}

// 为父亲添加一个生小孩的方法
func (f *Father) Make(name string) {
        fmt.Printf("%s生小孩", name)
}

func main() {
        ch := &Child{
                name:"小明",
                Father:&Father{50000},
        }

        // 小明继承的财产为:50000
        fmt.Printf("%s继承的财产为:%d\n",ch.name, ch.money )

        // 小明生小孩
        ch.Make("小明")
}

小明结构体中并没有money字段和Make方法。这两个都是他父亲结构体中才有的,这样一来便实现了继承属性和方法。

结构体字段的可见性

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

结构体json序列化

package main

import (
        "encoding/json"
        "fmt"
)

// 学生结构体
type Student struct {
        ID      int
        Gender  string
        Name    string
        IsLogin bool
}

func main() {
        var stu1 = Student{
                ID:      1,
                Gender:  "男",
                Name:    "豪杰",
                IsLogin: false,
        }

        // 序列化:把内存里的数据转换为字符串,以便用于网络传输和数据交换
        if v, err := json.Marshal(stu1); err != nil {
                fmt.Println("JSON格式化出错")
                fmt.Println(err)
        } else {
                // 字节类型的切片
                fmt.Println(v)
                // 转为字符串
                fmt.Println(string(v))
        }

        // 反序列化:把json字符串转换为当前编程语言可用的对象
        str := `{"ID":1,"Gender":"男","Name":"豪杰","IsLogin":false}`
        stu2 := new(Student)
        _ = json.Unmarshal([]byte(str), stu2)
        fmt.Println(stu2)
        fmt.Printf("stu2 类型:%T\tstu2值:%#v", stu2, stu2)
}

结构体的标签

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

`key1:"value1" key2:"value2"`

结构体标签由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。键值对之间使用一个空格分隔。

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

因为序列化和反序列化大多是与前端进行数据交互。不能给他返回一些大写的键。所以可以通过tag来解决。

package main

import (
        "encoding/json"
        "fmt"
)

//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!")
        }

        //json str:{"id":1,"Gender":"男"}
        fmt.Printf("json str:%s\n", data)
}