Go基础编程 - 08 - 结构体

上一篇:字典及其约束

下一篇:通道


Go语言中没有“类”的概念,也不支持类的“继承”等面向对象的概念,它所做的是通过嵌入字段的方式实现了类型之间的组合。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

1. 自定义类型、类型别名

1.1. 自定义类型

通过Type关键字定义。
自定义类型是定义一个全新的类型。可以基于基本类型定义,也可以通过struct定义。

// 通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。
type MyInt int

1.2. 类型别名

使用 “=” 定义,type TypeAlias = Type
类型别名规定:TypeAlias只是Type的别名,本质上属于同一个类型。

    type intAlias = int

    // 内置别名类型
    type byte = uint8
    type rune = int32
    type any = interface{}

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

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}

上面代码结果显示 a 的类型是 main.NewInt,表示 main 包下定义的 NewInt 类型;b 的类型是 int。MyInt 类型只会在代码中存在,编译完成时并不会有 MyInt 类型。

2. 结构体定义

  • 结构体是一种自定义类型,它是由若干字段组成的。
  • 结构体的字段可以是任意类型,甚至可以是结构体本身。
  • 结构体第一个字段的地址和结构体的指针是相同的。应把频繁执行字段放在第一个,这样能够减少 CPU 指令,提升性能。

通过 struct 关键字定义。

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

类型名:同一个包内不能重复
字段名:同一个结构体内不能重复
字段类型:可以是任意类型,甚至可以是结构体本身
// 定义一个结构体   
type Student struct {
    Name string
    Age  int
}   

3. 结构体初始化

只有当结构体实例化时,才会分配内存。也就是必须实例化后才能使用结构体的字段。

未赋值字段,初始化为字段类型零值。

var stu Student   
fmt.Printf("%#v\n", stu)    // main.Student{Name:"", Age:0} 

// 通过点符号(.)访问结构体字段
stu.Name = "Tom"   
fmt.Println(stu.Name)

使用键值对初始化

stu := Student{Name: "小花", Age: 18}
fmt.Println(stu)

使用值的列表初始化(简写:初始化时不写键,只写值)
需满足一下条件:

  1. 必须初始化结构体的所有字段。
  2. 初始值的填充顺序必须与字段在结构体中的声明顺序一致。
  3. 该方式不能和键值初始化方式混用。
stu := Student{"小明", 16}
fmt.Println(stu)

4. 指针类型结构体

初始化方式与值类型结构体相同,但使用指针类型进行初始化。

使用 new 关键字实例化

// new
var stu1 = new(Student)
fmt.Printf("%T \n", stu1)

使用 & 对结构体进行取址操作,相当于 new

stu := &Student{}
stu.Name = "小明"
(*stu).Age = 6
fmt.Printf("%#v\n", stu)

Go语法糖:会适时地为我们进行自动地转译,在stu之上,之所以我们可以通过stu.Name = "小明"设置名字,是因为 Go语言把它自动转译为了(*stu).Name = "小明"

5. 构造函数

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

type Student struct {
    Name string
    Age  int
}

func NewStudent(name string, age int) *Student {
    return &Student{Name: name, Age: age}
}

func main() {
    stu := NewStudent("小花", 18)
    fmt.Printf("%#v\n", stu)
}

6. 方法和接收者

Go语言中通过struct来实现面向对象;可以包含方法,方法是结构体的成员函数。

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

方法的接收者类型必须是某个自定义的数据类型,而且不能是接口类型或接口的指针类型。

函数与方法的区别:函数不属于任何类型,而方法属于特定的类型。

6.1. 方法定义

方法定义格式如下:

func (接收者 变量名) 方法名(参数列表)(返回值列表) {
    方法体
}

例如:

type Student struct {
    Name string
    Age  int
}

func (stu *Student) SayHello(age int) {
    fmt.Printf("Hello, my name is %s, and %d years old.\n", stu.Name, age)
} 

6.2. 方法调用

Go语言方法表达式有两种:instance.method(args…), .func(instance, args…)

func main() {
    stu := &Student{
        Name: "Tom",
        Age:  18,
    }
    stu.SayHello(18)

    // 隐式隐式
    sSay := stu.SayHello
    sSay(20)

    // 显式传递
    sr := (*Student).SayHello
    sr(stu, 22)
}

6.3. 值方法和指针方法

值方法:接收者类型是非指针的自定义数据类型的方法。

指针方法:接收者类型是指针的自定义数据类型的方法。

值方法与指针方法区别:

  1. 接受者类型:

    • 值方法的接收者是该方法所属类型值的一个副本,在方法内对该副本的修改一般不会体现在原值上,除非这个类型本身是某个引用类型(slice、map、chan)。
    • 指针方法的接收者是该方法所属类型值的指针的一个副本,在方法内对该副本指向的值进行修改一定会体现在原值上。
  2. 方法集合:一个自定义数据类型的方法集合中仅包含它的所有值方法;而该类型的指针类型的方法集合囊括了所有值方法和所有指针方法。

package main

import "fmt"

type Student struct {
    Name string
    Age  int
}

// 指针方法
func (stu *Student) SetName(name string) {
    stu.Name = name
}

// 值方法
func (stu Student) SetNameCopy(name string) {
    stu.Name = name
}

func main() {
    stu := &Student{"小红", 20}

    stu.SetName("小花")
    fmt.Println(stu.Name) // 小花

    stu.SetNameCopy("小明")
    fmt.Println(stu.Name) // 小花
}

6.4. 指针方法使用场景

  1. 需要修改接收者中的值;
  2. 接收者是拷贝代价比较大的对象;
  3. 保证一致性,如果某个方法使用了指针接收者,那么其它方法也应该使用指针方法。

6.5. 任意类型添加方法

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

举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

//MyInt 将int定义为自定义MyInt类型
type MyInt int

//SayHello 为MyInt添加一个SayHello的方法
func (m MyInt) SayHello() {
   fmt.Println("Hello, 我是一个int。")
}

func main() {
   var m MyInt
   m.SayHello() //Hello, 我是一个int。
   m = 100
   fmt.Printf("%#v  %T\n", m, m) //100  main.MyInt
}

7. 结构体成员可见性

Go语言通过首字母的大小写来控制权限。首字母大写包外部可见,首字母小写仅包内部可见。

demo/struct.go

package demo

type Student struct {
    Name  string    // 公共字段
    Age   int   
    score int   // 包内字段,在包外部不可初始化
}

// 公共方法
func (stu *Student) SetName(name string) {
    stu.Name = name
}

// 包内方法
func (stu *Student) setScore(score int) {
    stu.score = score
}

main.go

package mian

import (
    "demo"
    "fmt"
)

func main() {
    stu := &demo.Student{Name: "小花", Age: 18}

    stu.SetName("小红")
    stu.setScore(80)    // stu.setScore undefined (type *demo.Student has no field or method setScore)

    fmt.Println(stu)
}

8. 结构体匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,那么它就是一个嵌入字段,也可以被称为匿名字段。

匿名字段默认采用类型作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个。

type BaseType struct {
    string
    int
}

func main() {
    var b = BaseType{}
    b.int = 10
    fmt.Println(b.int)
}

9. 嵌套结构体

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

type Address struct {
    Province string
    City     string
}

type User struct {
    Name    string
    Gender  string
    Address Address
}

func main() {
    user := User{
        Name:   "pprof",
        Gender: "女",
        Address: Address{
            Province: "北京",
            City:     "北京",
        },
    }
    fmt.Printf("user=%#v\n", use)
}

9.1. 嵌套匿名结构体

嵌套结构体字段也可以使用匿名。推荐使用匿名方式嵌套结构体,如下所示:

type User struct {
    Name    string
    Gender  string
    Address
}

func main() {
    user := User{Name:"pprof", Gender:"女"}
    user.Address.Province = "北京"
    fmt.Printf("user=%#v\n", use)
}

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

嵌套结构体内部可能存在相同的字段名,为了避免歧义需要指定具体的内嵌结构体的字段。

仅匿名内嵌结构体成员可以直接访问。Go直接访问成员匹配规则:

  1. 结构体成员中含有要访问的成员,则匹配结构体成员。
  2. 结构体成员中没有要访问的成员,则在内嵌匿名结构体中继续查找。若同一层级多个内嵌匿名结构体存在同名成员,则产生歧义,报错。
  3. 内嵌结构体中未找到要访问的成员,若内嵌匿名结构体内还存在内嵌匿名结构体,则继续向下查找,依此类推。
  4. 都匹配不到则报错。
package main

type Student struct {
    Name       string
    Age        int
    CreateTime string
    Address
    Email
    }

type Email struct {
    Account    string
    Phone      string
    CreateTime string
}

type Address struct {
    Name       string
    Province   string
    City       string
    County     string
    Phone      string
    CreateTime string
}

func (stu *Student) SetName(name string) {
    stu.Name = name
}

func (addr *Address) SetName(name string) {
    addr.Name = name
}

func (addr *Address) SetProvince(pro string) {
    addr.Province = pro
}

func main() {

    var stu = &Student{}

    // 1. 嵌套结构体重名字段,需自定具体路径
    stu.CreateTime = "2023-12-21"
    stu.Address.CreateTime = "2023-12-30"

    // 2. 直接访问匿名结构体的字段名(当访问结构体成员时会先在结构体中查找该字段,找不到再去匿名结构体中查找。)
    stu.Account = "123@mail.com"

    // 3. 向下查找字段若同一层级出现重名字段,则产生歧义;需指定具体结构体字段。
    stu.Phone = "123123123" //报错:ambiguous selector stu.Phone
    stu.Address.Phone = "123123"

    // 结构体方法处理方式与字段相同
    stu.SetName("老北京")
    stu.Address.SetName("小北京")
    stu.SetProvince("北京市")

    fmt.Printf("%#v", stu)
}

10. 通过嵌套实现“继承”

type Animal struct {
    name string
}

func (a *Animal) move() {
    fmt.Printf("%s会动!\n", 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() //旺财会动!
}

11. 结构体与JSON序列化

package main

import (
    "encoding/json"
    "fmt"
)

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

type Student struct {
    Name  string
    Age   int
    Address
}

type Address struct {
    Province string
    City     string
    County   string
}

func main() {
    c := &Class{
        Title:    "高一(12)班",
        Students: make([]*Student, 0, 60),
    }

    for i := 1; i <= 10; i++ {
        stu := &Student{
            Name:    fmt.Sprintf("Stu_%02d", i),
            Address: Address{Province: "北京"},
        }
        c.Students = append(c.Students, stu)
    }

    // JSON序列化:结构体 to JSON
    data, err := json.Marshal(c)
    if err != nil {
        fmt.Println("Json marshal failed.")
        return
    }
    fmt.Printf("json:%s\n\n\n", data)

    // JSON反序列化:JSON to 结构体
    str := `{"Title":"101","Students":[{"Name":"小明","Age":16,"Address":{"Province":"北京"}},{"Name":"小花","Age":15,"Address":{"Province":"上海"}},{"Name":"小红","Age":16,"Address":{"Province":"广东"}}]}`
    c1 := &Class{}
    err = json.Unmarshal([]byte(str), c1)
    if err != nil {
        fmt.Println("json unmarshal failed!")
        return
    }
    fmt.Printf("%#v\n", c1)
}

12. 结构体标签(Tag)

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

结构体Tag在字段的后方定义,使用一对反引号(``)包括起来;由一个或多个键值对组成,键与值使用冒号分隔,值用双引号括起来,键值对之间使用一个空格分隔。

具体格式如下:

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

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

在结构体字段的定义中,如果字段名与 json 标签不一致,则需要在字段名后面添加一个 json 标签,如:

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    Id    int    `json:"id"` //通过指定tag实现json序列化该字段时的key
    Name  string `json:"name"`
    Age   int    `json:"-"` //通过指定tag实现json序列化该字段时忽略该字段
    Sex   string  `json:"name,omitempty"` // omitempty表示字段为空值时不序列化
    phone string //私有不能被json包访问
}

func main() {
    stu := Student{
        Id:    1,
        Name:  "小明",
        Age:   18,
        phone: "13333333333",
    }
    str, _ := json.Marshal(stu)

    fmt.Printf("json:%s\n", str) //json:{"id":1,"name":"小明"}

    str2, _ := json.Marshal(Student{
        Id:  2,
        Sex: "女",
    })
    fmt.Printf("json:%s\n", str2) //json:{"id":2,"name":"","sex":"女"}
}

13. 匿名结构体

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

package main

import (
    "fmt"
)

func main() {
    var user struct{Name string; Age int}
    user.Name = "pprof.cn"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值