【From C To Go】1.4 结构体与方法


Go语言没有当下面向对象语言中最流行的类与对象的概念,而是使用和C语言一样的结构体。因此,Go语言中不存在继承这种面向对象语言中复杂的特性,而是采用类型嵌入(或称组合)代替,“组合优于继承”也是Go语言的编程哲学之一

1. struct基本操作

1.1 声明与初始化

1.1.1 声明

声明结构体需要使用typestruct关键字,type用来表明这是这是一个用户自定义的数据类型,struct之后便是结构体的定义,如下:

type Person struct {
	Age	 int
	Name string
}

我们便声明了一个包含Age和Name两个字段的结构体Person

1.1.2 初始化

结构体的初始化有以下方式:

  1. 使用字段名初始化
  2. 依次提供字段值初始化
  3. 使用new函数创建指针并初始化
  4. 使用&符号创建指针
	// 使用字段名初始化
	p1 := Person{Name: "ZhangSan", Age: 18}
	// 依次提供字段值初始化
	p2 := Person{20, "LiSi"}
	// 使用new函数创建指针并初始化
	p3 := new(Person)
	p3.Age = 23
	p3.Name = "WangWu"
	// 使用&创建结构体指针变量
	p4 := &Person{Name: "ZhaoLiu", Age: 24}
	fmt.Println(p1, p2, p3, p4)
	fmt.Printf("p1 type : %T, p2 type : %T, p3 type : %T, p4 type : %T\n", p1, p2, p3, p4)

示例代码运行结果如下:
在这里插入图片描述

1.2 结构体组合

Go语言虽然不直接支持继承,但可以通过结构体嵌套的方式来实现类似继承的设计。其目标都是一致的:实现代码复用和层次结构
Go语言结构体中嵌套的结构体可以具名,也可以匿名(对于具有变量名的嵌套结构体,外层结构体无法直接访问内层结构体的字段)

type Student struct {
	Person
	School string
}

type Employee struct {
	p       Person
	Company string
}

func StructCompose() {
	s := Student{
		Person: Person{
			Name: "ZhangSan",
			Age:  18,
		},
		School: "BeiJing",
	}
	fmt.Println(s)
	fmt.Println(s.Name)
	fmt.Println(s.Person.Age)
	e := Employee{
		p: Person{
			Name: "ZhangSan",
			Age:  18,
		},
		Company: "CCB",
	}
	// 具名字段不能直接使用e.Name
	fmt.Println(e)
	// fmt.Println(e.Name)
	fmt.Println(e.p.Name)
}

该例子运行结果如下:

字段遮蔽

  • Go语言外层结构体和内层结构体允许具有相同名称的字段,此时内层结构体的相应变量会被外层结构体遮蔽,避免产生混淆,如下例程:
type Teacher struct {
	Person
	Age string
}

func StructTest() {
	t := Teacher{
		Person: Person{
			Age:  44,
			Name: "LiSi",
		},
		Age: "44",
	}
	fmt.Println(t)
	fmt.Println(t.Name)
	fmt.Println(t.Person.Age)
	fmt.Println(t.Age)
	fmt.Printf("t.Age type : %T\n", t.Age)
}

该例程运行结果如下:可见,t.Age的类型是外层结构体Teacher的string类型的Age,而非嵌套的Person结构体的int类型的Age字段
在这里插入图片描述

2. 方法与函数

之前刚接触到Go语言的方法和函数时,我一直很困惑这俩长得看起来几乎一模一样的东西区别到底是什么,直到有一天,我看到了一句话:“方法的受体可以看做java,C++等语言中的this指针”,瞬间茅塞顿开

2.1 声明

如前所述,方法与函数的区别仅在于是否具有受体(receiver)这一额外的成员,其他地方都是一致的:由func关键字、方法(函数)名、参数、返回值、函数体组成,如下:

// 函数声明
func Add(a, b int) int {
	return a + b
}

// 方法声明
func (p *Person) Add(a, b int) int {
	return a + b
}

此两者的区别仅在于方法通过方法受体将方法与类型进行了绑定,效果类似于面向对象语言中定义在类内部的成员方法

2.2 方法受体

再次重申:方法是可以附加到某个类型上的函数,这个类型可以是任何命名类型(包括结构体、自定义的基本类型等)。方法受体用于指定方法属于哪个类型,并决定了该方法可以操作的具体实例。方法受体可以是值或指针。

2.2.1值传递

传值作为受体时,修改的是传递进来的受体的副本,不会影响到原始结构体的值。适用于不需要修改原始结构体值的方法

func (p Person) AgePlus(age int) int {
	p.Age += age
	return p.Age
}

该函数不会改变p的Age字段

2.2.2 指针传递

与值传递相反,如果需要修改原始结构体的值,方法就需要使用结构体指针作为受体。如下:

func (p *Person) AgePlus2(age int) int {
	p.Age += age
	return p.Age
}

使用以下函数进行验证:

func StructMethodTest() {
	p := Person{
		Age:  18,
		Name: "ZhangSan",
	}
	fmt.Println(p.AgePlus(10))
	fmt.Println(p.Age)
	fmt.Println(p.AgePlus2(10))
	fmt.Println(p.Age)
}

该函数输入如下:可以看到,指针作为方法受体的方法AgePlus2确实改变了p的Age字段的值
在这里插入图片描述

2.2.3 自动转换

调用指针接收者方法时,可以使用值调用,Go会自动进行地址取值操作;同样地,调用值接收者方法时,可以使用指针调用,Go会自动进行解引用操作。

func StructMethodTest() {
	p1 := Person{
		Age:  18,
		Name: "ZhangSan",
	}
	p2 := &Person{
		Age:  19,
		Name: "Lisi",
	}
	fmt.Println(p1.AgePlus(2))
	fmt.Println(p1.AgePlus2(2))
	fmt.Println(p2.AgePlus(2))
	fmt.Println(p2.AgePlus2(2))
}

以上使用方式都是没有问题的
在这里插入图片描述

3. 底层实现

在标准库reflect包中定义了几个重要的类型和方法,用于表示和操作Go语言中的类型信息。这些类型包括TypeValueStructField等。

3.1 reflect.Type和reflect.Value

  • reflect.Type:表示一个Go类型。
  • reflect.Value:表示一个Go值。

通过reflect.Typereflect.Value,可以获取有关类型和值的详细信息,包括字段标签。

3.2 reflect.StructField

StructFieldreflect包中的一个结构体类型,表示结构体的一个字段。它的定义如下:

type StructField struct {
    Name      string
    PkgPath   string
    Type      Type      // 字段的类型
    Tag       StructTag // 字段的标签
    Offset    uintptr
    Index     []int
    Anonymous bool
}
  • Name:字段的名称。
  • PkgPath:字段的包路径,对于导出字段,PkgPath是空字符串。
  • Type:字段的类型。
  • Tag:字段的标签,以StructTag类型表示。
  • Offset:字段在结构体中的字节偏移量。
  • Index:字段的索引。
  • Anonymous:是否是匿名字段。

3.3 reflect.StructTag

StructTag是一个用于表示字段标签的字符串类型。它的定义如下:

type StructTag string

StructTag类型提供了一些方法来解析和获取标签的内容,例如:

  • Get(key string) string:返回标签中指定键的值。
  • Lookup(key string) (value string, ok bool):返回标签中指定键的值和一个布尔值,表示该键是否存在。

示例:通过reflect包访问结构体字段标签

以下是一个示例,展示如何通过reflect包访问结构体字段的标签:

package main

import (
    "fmt"
    "reflect"
)

type User struct {
    Name  string `json:"name" validate:"required"`
    Email string `json:"email"`
    Age   int    `json:"age,omitempty"`
}

func main() {
    u := User{Name: "Alice", Email: "alice@example.com", Age: 30}
    t := reflect.TypeOf(u)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field Name: %s\n", field.Name)
        fmt.Printf("JSON Tag: %s\n", field.Tag.Get("json"))
        fmt.Printf("Validate Tag: %s\n", field.Tag.Get("validate"))
        fmt.Println()
    }
}

运行结果:
在这里插入图片描述
在上面的示例中,我们定义了一个User结构体,其中包含字段标签。通过reflect.TypeOf获取User的类型信息,并通过遍历字段(t.Field(i)),我们可以访问每个字段的标签信息(field.Tag.Get("json")field.Tag.Get("validate"))。

这些标签在运行时通过反射机制被解析,用于各种用途,如序列化、反序列化、验证、数据库映射等。

3.4 Tag的本质

标签(tag)在Go语言中是结构体字段声明后面的字符串字面量,用于为字段附加元数据。标签本质上是字符串,通过反引号括起来。使用reflect包的StructFieldTag字段可以访问这些标签。

标签在定义时的格式是key:"value",并且可以包含多个键值对,用空格分隔。

3.5 Tag的用途

字段标签在Go语言中有多种用途,常见的用途包括:

  1. 序列化和反序列化

    字段标签最常见的用途是指定结构体字段在序列化和反序列化过程中的映射关系。常见的库包括encoding/jsonencoding/xmlgopkg.in/yaml.v2等。

    type User struct {
        Name  string `json:"name"`
        Email string `json:"email"`
        Age   int    `json:"age,omitempty"`
    }
    

    在上面的例子中,json标签指定了JSON键与结构体字段的映射关系。omitempty表示当字段的值为空值时,序列化时可以省略该字段。

  2. 数据库映射

    在使用ORM(对象关系映射)库时,字段标签用于映射数据库表中的列。

    type User struct {
        ID    int    `db:"id"`
        Name  string `db:"name"`
        Email string `db:"email"`
    }
    

    在上面的例子中,db标签指定了数据库列名。

  3. 验证

    字段标签可以用于验证框架,如go-playground/validator,来指定字段的验证规则。

    type User struct {
        Name  string `validate:"required"`
        Email string `validate:"required,email"`
        Age   int    `validate:"gte=0,lte=130"`
    }
    

    在上面的例子中,validate标签指定了字段的验证规则。

  4. 其他自定义用途

    字段标签还可以用于其他自定义用途,只要在需要的地方解析这些标签即可。例如,生成API文档、指定表单字段名称等。

  • 30
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值