Go struct 理解与应用

584 阅读8分钟

Go 语言没有遵循传统的面向对象编程,也没有类-对象的体系结构。

那是不是说 Go 不具备面向对象语言的特性呢?

当然不是了,因为它使用了更加灵活的“结构体”替代了“”。

Go 语言通过自定义的方式声明新的类型,而结构体就是这些类型中的一种复合类型。

那什么是结构体呢?

结构体简介

Go 语言通过自定义的方式形成新的类型,而结构体就是这些类型中的一种复合类型。结构体是由一个或者多个任意类型的值聚合成的实体,也就是说由一系列具有相同类型或者不同类型的数据构成的数据集合。

假如需要一个学生的姓名和年龄,我们可以创建姓名和年龄两个变量来存储这些值。而现在我们有一个班级的学生,并且都要存储相应的信息。这就是结构体要发挥的作用了。

结构体的定义

结构体定义格式如下:

type TypeName struct {
    field1 dataType1
    field2 dataType2
    ...
}

需要注意的是, 类型名称(TypeName)在同一个包内是不能重复的。

关键字type可以将各种基本类型定义为自定义类型,基本类型包括整形、字符串、布尔等。

结构体的实例化

结构体的定义只是一种内存布局的描述,只有对结构体进行实例化后,才会真正的分配内存,并且才可以访问结构体的字段。

在结构体实例化的时候,为每个成员设置相应的数值,如果没有为某个值设置,则会使用默认值。

type Student struct{
	name string
    age int
}

// 实例化方式一: 实例化时初始化默认值
s := Student{name: "Jim", age: 19}
fmt.Println("name=", s.name, " age=", s.age)

// 实例化方式二:先实例化结构体,再赋值
var s1 Student
s1.name = "Tom"
s1.age = 21

// 实例化方式三: 使用 new() 实例化结构化
s2 := new(Student)
s2.name = "Jsaon"
s2.age = 22

// 实例化方式四:取结构体实例化的内存地址
s3 := &Student{name:"Swift", age: 22}
fmt.Println("name=", s3.name, " age=", s3.age)

在实例化结构体的时候,除了常规则的实例化方式外,还可以使用内置函数方法new()和取地址操作符“&”实现。

这两种实例化方法都是由指针方式完成的,在访问成员的时候也是使用实心点,但编译器自动将其转换为(*yyy).xxx形式访问。
例如:

var s3 *Student = new (Student)
(*s3).name = "John"
(*s3).age = 18

var s4 *Stuent = &Student{}
(*s4).name = "John"
(*s4).age = 18

结构体在实例化并初始化值时,支持三种形式:

  • 通过.来访问结构体的字段并初始化值
  • 通过<key-value>的形式对结构体赋值
  • 通过无键顺序赋值,即按照结构体字段顺序进行赋值

使用第三种无键赋值时,需要注意的是:

  • 必须初始化结构体的所有字段
  • 值的顺序必须与结构体中字段的顺序一致
  • 不能与键值方式混用

不过,这种方式还有一个致命的缺点,就是一旦结构体类型增加了一个新的字段,即使是未导出的,这种值构造方式将会导致编译失败。

也就是说,一旦我们需要在结构体新增字段,那么使用该方式赋值的都需要进行相应的调整,也因此,Go推荐使用field:value的复合字面值形式对struct类型变量进行值构造,这种值构造方式可以降低结构体类型使用者与结构体类型设计者之间的耦合。

结构体标签

结构体中的字段除了名字和类型外,还有一个可选的标签(tag),它是一个附属于字段的字符串,可以用作一些重要的标记,而标记的内容只有 reflect 包可用。

比如解析JSON就需要使用内置 包 encoding/json,它为我们提供了一些默认的标签;还有一些开源的ORM框架也广泛使用结构体的标签,比如 gorm

我们可以看下Json字符串与Struct类型相互转换的示例:

package main

import (
	"encoding/json"
	"fmt"
)

type Student struct {
	Name string `json:"name"`
	Age  int    `json:"age"`
}

func main() {

	var s Student	
	tj := `{"name": "xiaoming", "age": 29}`

	_ := json.Unmarshal([]byte(tj), &s)
	fmt.Println(s)   // output: {xiaomings 29}

	newjson, _ := json.Marshal(s)
	fmt.Println(string(newjson)) // output: {"name":"xiaomings","age":29}
}

这里主要就是利用结构体中的对应字段的tag,json解析的原理就是通过反射获得每个字段的tag,然后把解析的json对应的值赋给他们。

结构体标签设置规则

  • 标签在字段的数据类型后面设置,以字符串形式表示,并使用反引号表示字符串
  • 标签内容格式由一个或多个键值对组成,键与值 使用:分隔(注意,不能留有空格),值使用""引起来,若有多个键值对,则它们之间使用一个空格分隔。

我们还需要注意的是,如果结构体中的字段名称首字母是小写格式的话,则表示该字段不可导出,我们使用encoding/json就会无法获取这个字段的数据了。

匿名结构体与匿名成员

匿名结构体

匿名结构体有两种使用方式,第一种是使用关键字var定义;第二种使用:=定义,并为匿名结构体成员设置初始值。例如

package main
import "fmt"

func main () {
	// 定义匿名结构体
    var p struct {
        name string
        age  int
    }
    p.name = "John"
    p.age = 22

    // 第二种方式
    p1 := struct {
        name string
        age int
    }{
        name: "Tom",
        age : 24,
    }
}

注意,使用匿名结构体必须赋值给变量,否则没有办法使用。

匿名成员

匿名成员在结构体中没有明确的定义成员名称,只定义了成员的数据类型。例如

type Exam struct {
    string
    int
    bool
}

c := Exam{"Time", 110, true}

我们以c.string``c.int的形式来访问匿名字段的值,但是需要注意,由于是匿名字段,基本类型是不可以重复的,也就是说,只能存在一个string类型。

结构体匿名成员的数据类型只能为字符串、整型、浮点型、复数或布尔型等基本数据类型,不能直接支持数组、切片、Map和结构体等复合类型。但是结构体可以通过嵌套的形式来实现匿名字段。

比如:

type Point struct {
    X int
    Y int
}

type Circle struct {
    Point
    Area float64
}

这种形式的实现其实是Go语言中的实现继承的另一种体现方式,如上例,Circle的实例将拥有(继承)Point类型实现的方法。

那么这种匿名结构体会有什么用处呢? 在两种情况下匿名结构体非常有用,第一种情况是将外部数据转换为结构体或者结构体转换为外部数据(如JSON或者协议缓冲区),这个过程也被称为序列化数据或反序列化数据。第二种情况是在编写单元测试时也经常会用到匿名结构体。

结构体嵌套

结构体也是一种数据类型,所以它也可以作为一个匿名字段来使用。
内嵌结构体有以下特点:

  • 内嵌的结构体可以直接访问其成员变量。前提条件是:必须以匿名的形式嵌套。例如:Circle.Point.X可以简化为Circle.X
  • 内嵌结构体的字段名就是它的类型名。可以使用详细的字段进行层层访问。

注意:一个结构体只能嵌入一个同类型的成员,不需要担心结构体重名和错误赋值的情况,编译器在发现有赋值歧义的情况时会提示报错。

结构体嵌套一般应用在同类属性字段的封装,可以以另外一种途径实现“继承”或者在使用链表的时候也会用到结构体嵌套。

结构体方法

在Go语言中,方法是与特定类型相关联的函数。方法允许我们在自定义类型上定义行为,这些行为可以被外部代码调用和使用。在本质上,方法是一种特殊类型的函数,它需要一个接收器(receiver),即方法被调用的结构体实例。

在Go语言中,方法可以定义在结构体上,也可以定义在任何自定义类型上。以下是一个简单的结构体定义及其方法:

type Person struct {
    Name string
    Age int
}

func (p Person) Greet() {
    fmt.Printf("Hello, my name is %s and I'm %d years old.\n", p.Name, p.Age)
}

在上面的代码中,我们定义了一个名为Person的结构体,其中包含名字和年龄字段。我们还定义了一个名为Greet的方法,该方法用于在控制台上打印出个人的问候语。

注意到Greet方法的定义有一个Person类型的接收器,它告诉编译器这个方法是在Person类型上定义的。当我们调用Greet方法时,它将自动传递一个Person类型的接收器,这个接收器可以在方法内部使用。

以下是如何使用这个Person类型和它的Greet方法:

func main() {
    p := Person{Name: "John", Age: 30}
    p.Greet() // 输出 "Hello, my name is John and I'm 30 years old."
}

在上面的代码中,我们首先创建了一个名为pPerson类型的实例。然后,我们调用了Greet方法来打印问候语,这个方法使用了Person类型的实例作为接收器。

总的来说,方法允许我们在自定义类型上定义行为,并且可以让我们更好地组织和抽象代码。在Go语言中,方法和函数一样,是一等公民,可以作为变量、参数和返回值进行传递。