Go语言学习9-结构体类型

简介: 【4月更文挑战第8天】本篇 Huazie 向大家介绍 Go语言的接口类型

18a325e136e34114b15a6d2caa5b02de.jpeg

引言

书接上篇,我们了解了Go语言的《接口类型》,现在介绍Go语言的结构体类型。主要如下:

主要内容

结构体类型既可以包含若干个命名元素(又称字段),又可以与若干个方法相关联。

1. 类型表示法

结构体类型的声明可以包含若干个字段的声明。字段声明左边的标识符表示了该字段的名称,右边的标识符代表了该字段的类型,这两个标识符之间用空格分隔。

结构体类型声明中的每个字段声明都独占一行。同一个结构体类型声明中的字段不能出现重名的情况。

结构体类型也分为命名结构体类型匿名结构体类型

命名结构体类型

命名结构体类型以关键字type开始,依次包含结构体类型的名称、关键字struct和由花括号括起来的字段声明列表。如下:

type Sequence struct {
   
    len int
    cap int
    Sortable
    sortableArray  sort.Interface
}

结构体类型的字段的类型可以是任何数据类型。当字段名称的首字母是大写字母时,我们就可以在任何位置(包括其他代码包)上通过其所属的结构体类型的值(以下简称结构体值)和选择表达式访问到它们。否则当字段名称的首字母是小写,这些字段就是包级私有的(只有在该结构体声明所属的代码包中才能对它们进行访问或者给它们赋值)。

如果一个字段声明中只有类型而没有指定名称,这个字段就叫做匿名字段。如上结构体 Sequence 中的 Sortable 就是一个匿名字段。匿名字段有时也被称为嵌入式的字段或结构体类型的嵌入类型。

匿名字段的类型必须由一个数据类型的名称或者一个与非接口类型对应的指针类型的名称代表。代表匿名字段类型的非限定名称将被隐含地作为该字段的名称。如果匿名字段是一个指针类型的话,那么这个指针类型所指的数据类型的非限定名称(由非限定标识符代表的名称)就会被作为该字段的名称。非限定标识符就是不包含代码包名称和点的标识符。

匿名类型的隐含名称的实例,如下:

type Anonymities struct {
   
    T1
    *T2
    P.T3
    *P.T4
}

这个名为 Anonymities 的结构体类型包含了4个匿名字段。其中,T1P.T3 为非指针的数据类型,它们隐含的名称分别为 T1T3*T2*P.T4 为指针类型,它们隐含的名称分别为 T2T4

注意:匿名字段的隐含名称也不能与它所属的结构体类型中的其他字段名称重复。

结构体类型中的嵌入字段的类型所附带的方法都会成为该结构体类型的方法,结构体类型自动实现了它包含的所有嵌入类型所实现的接口类型。但是嵌入类型的方法的接收者类型仍然是该嵌入类型,而不是被嵌入的结构体类型。当在结构体类型中调用实际上属于嵌入类型的方法的时候,这一调用会被自动转发到这个嵌入类型的值上。

现在对 Sequence 的声明进行改动,如下:

type Sequence struct {
   
    Sortable
    sorted bool
}

上面的 Sequence 中的匿名字段 Sortable 用来存储和操作可排序序列,布尔类型的字段 sorted 用来表示类型值是否已经被排序。

假设有一个 Sequence 类型的值 seq,调用 Sortable 接口类型中的方法 Sort,如下:

seq.Sort()

如果 Sequence 类型中也包含了一个与 Sortable 接口类型中的方法 Sort 的名称和签名相同的方法,那么上面的调用一定是对 Sequence 类型值自身附带的 Sort 方法的调用,而嵌入类型 Sortable 的方法 Sort 被隐藏了。

如果需要在原有的排序操作上添加一些额外功能,可以这样声明一个同名的方法:

func (self *Sequence) Sort() {
   
    self.Sortable.Sort()
    self.sorted = true
}

这样声明的方法实现了对于匿名字段 SortableSort 方法的功能进行无缝扩展的目的。

如果两个 Sort 方法的名称相同但签名不同,那么嵌入类型 Sortable 的方法 Sort 也同样会被隐藏。这时,在 Sequence 的类型值上调用 Sort 方法的时候,必须依据该 Sequence 结构体类型的 Sort 方法的签名来编写调用表达式。如下声明 Sequence 类型附带的名为 Sort 的方法:

func (self *Sequence) Sort(quicksort bool) {
   
    //省略若干语句
}

但是调用表达式 seq.Sort() 就会造成一个编译错误,因为 Sortable 的无参数的 Sort 方法已经被隐藏了,只能通过 seq.Sort(true)seq.Sort(false) 来对 SequenceSort 方法进行调用。

注意:无论被嵌入类型是否包含了同名的方法,调用表达式 seq.Sortable.Sort() 总是可以来调用嵌入类 SortableSort 方法。

现在,区别一下嵌入类型是一个非指针的数据类型还是一个指针类型,假设有结构体类型 S 和非指针类型的数据类型 T,那么 *S 表示指向 S 的指针类型,*T 表示指向 T 的指针类型,则:

  1. 如果在 S 中包含了一个嵌入类型 T,那么 S*S 的方法集合中都会包含接收者类型为 T 的方法。除此之外,*S 的方法集合中还会包含接收者类型为 *T 的方法。

  2. 如果在 S 中包含了一个嵌入类型 *T,那么 S*S 的方法集合中都会包含接收者类型为 T*T 的所有方法。

现在再讨论另一个问题。假设,我们有一个名为 List 的结构体类型,并且在它的声明中嵌入了类型 Sequence,如下:

type List struct {
   
    Sequence
}

假设有一个 List 类型的值 list,调用嵌入的 Sequence 类型值的字段 sorted,如下:

list.sorted

如果 List 类型也有一个名称为 sorted 的字段的话,那么其中的 Sequence 类型值的字段 sorted 就会被隐藏。

注意: 选择表达式 list.sorted 只代表了对 List 类型的 sorted 字段的访问,不论这两个名称为 sorted 的字段的类型是否相同。和上面的类似,这里选择表达式 list.Sequence.sorted 总是可以访问到嵌入类型 Sequence 的值的 sorted 字段。

对于结构体类型的多层嵌入的规则,有两点需要说明:

  1. 可以在被嵌入的结构体类型的值上像调用它自己的字段或方法那样调用任意深度的嵌入类型值的字段或方法。唯一的前提条件就是这些嵌入类型的字段或方法没有被隐藏。如果它们被隐藏,也可以通过类似 list. Sequence.sorted 这样的表达式进行访问或调用它们。

  2. 被嵌入的结构体类型的字段或方法可以隐藏任意深度的嵌入类型的同名字段或方法。任何较浅层次的嵌入类型的字段或方法都会隐藏较深层次的嵌入类型包含的同名的字段或方法。注意,这种隐藏是可以交叉进行的,即字段可以隐藏方法,方法也可以隐藏字段,只要它们的名称相同即可。

如果在同一嵌入层次中的两个嵌入类型拥有同名的字段或方法,那么涉及它们的选择表达式或调用表达式会因为编译器不能确定被选择或调用的目标而造成一个编译错误。

匿名结构体类型

匿名结构体类型比命名结构体类型少了关键字type类型名称,声明如下:

struct {
   
    Sortable
    sorted bool
}

可以在数组类型、切片类型或字典类型的声明中,将一个匿名的结构体类型作为他们的元素的类型。还可以将匿名结构体类型作为一个变量的类型,例如:

var anonym struct {
   
    a int
    b string
}

不过对于上面,更常用的做法就是在声明以匿名结构体类型为类型的变量的同时对其初始化,例如:

anonym := struct {
   
    a int
    b string
}{
   0, "string"}

与命名结构体类型相比,匿名结构体类型更像是“一次性”的类型,它不具有通用性,常常被用在临时数据存储和传递的场景中。

在Go语言中,可以在结构体类型声明中的字段声明的后面添加一个字符串字面量标签,以作为对应字段的附加属性。例如:

type Person struct {
   
    Name    string `json:"name"`
    Age     uint8 `json:"age"`
    Address string `json:"addr"`
}

如上的字段的字符串字面量标签一般有两个反引号包裹的任意字符串组成。并且,它应该被添加但在与其对应的字段的同一行的最右侧。

这种标签对于使用该结构体类型及其值的代码来说是不可见的。但是,可以用标准库代码包 reflect 中提供的函数查看到结构体类型中字段的标签。这种标签常常会在一些特殊应用场景下使用,比如,标准库代码包 encoding/json 中的函数会根据这种标签的内容确定与该结构体类型中的字段对应的 JSON 节点的名称。

2. 值表示法

结构体值一般由复合字面量(类型字面量和花括号构成)来表达。在Go语言中,常常将用于表示结构体值的复合字面量简称为结构体字面量。在同一个结构体字面量中,一个字段名称只能出现一次。例如:

Sequence{
   Sortable: SortableStrings{
   "3", "2", "1"}, sorted: false}

类型 SortableStrings 实现了接口类型 Sortable,这个可以在Go语言学习笔记4中了解到。这里就可以把一个 SortableStrings 类型的值赋给 Sortable 字段。

编写结构体字面量,还可以忽略字段的名称,但有如下的两个限制:

  1. 如果想要省略其中某个或某些键值对的键,那么其他的键值对的键也必须省略。

    Sequence{
          SortableStrings{
         "3", "2", "1"}, sorted: false} // 这是不合法的
    
  2. 多个字段值之间的顺序应该与结构体类型声明中的字段声明的顺序一致,并且不能够省略掉任何一字段的赋值。但是不省略字段名称的字面量却没有此限制。例如:

    Sequence{
          sorted: false , Sortable: SortableStrings{
         "3", "2", "1"}} // 合法
    Sequence{
         SortableStrings{
         "3", "2", "1"}, false} // 合法 
    Sequence{
          Sortable: SortableStrings{
         "3", "2", "1"}} // 合法,未被明确赋值的字段的值会被其类型的零值填充。
    Sequence{
          false , SortableStrings{
         "3", "2", "1"}} // 不合法,顺序不一致,会编译错误
    Sequence{
          SortableStrings{
         "3", "2", "1"}} // 不合法,顺序不一致,会编译错误
    

在Go语言中,可以在结构体字面量中不指定任何字段的值。例如:

Sequence{
   } // 这种情况下,两个字段都被赋予它们所属类型的零值。

与数组类型相同,结构体类型属于值类型。结构体类型的零值就是如上的不为任何字段赋值的结构体字面量。

3. 属性和基本操作

一个结构体类型的属性就是它所包含的字段和与它关联的方法。在访问权限允许的情况下,我们可以使用选择表达式访问结构体值中的字段,也可以使用调用表达式调用结构体值关联的方法。

在Go语言中,只存在嵌入而不存在继承的概念。不能把前面声明的 List 类型的值赋给一个 Sequence 类型的变量,这样的赋值语句会造成一个编译错误。在一个结构体类型的别名类型的值上,既不能调用那个结构体类型的方法,也不能调用与那个结构体类型对应的指针类型的方法。别名类型不是它源类型的子类型,但别名类型内部的结构会与它的源类型一致。

对于一个结构体类型的别名类型来说,它拥有源类型的全部字段,但这个别名类型并没有继承与它的源类型关联的任何方法。

如果只是将 List 类型作为 Sequence 类型的一个别名类型,那么声明如下:

type List Sequence

此时,List 类型的值的表示方法与 Sequence 类型的值的表示方法一样,如下:

List{
    SortableStrings{
   "4", "5", "6"}, false}

如果有一个 List 类型的值 List,那么选择表达式 list.sorted 访问的就是这个 List 类型的值的 sorted 字段,同样,我们也可以通过选择表达式 list.Sortable 访问这个值的嵌入字段 Sortable。但是这个 List 类型目前却不包含与它的源类型 Sequence 关联的方法。

在Go语言中,虽然很多预定义类型都属于泛型类型(比如数组类型、切片类型、字典类型和通道类型),但却不支持自定义的泛型类型。为了使 Sequence 类型能够部分模拟泛型类型的行为特征,只是向它嵌入 Sortable 接口类型是不够的,需要对 Sortable 接口类型进行拓展。如下:

type GenericSeq interface {
   
    Sortable
    Append(e interface{
   }) bool
    Set(index int, e interface{
   }) bool
    Delete(index int) (interface{
   }, bool)
    ElemValue(index int) interface{
   }
    ElemType() reflect.Type
    value() interface{
   }
}

如上的接口类型 GenericSeq 中声明了用于添加、修改、删除、查询元素,以及获取元素类型的方法。一个数据类型要实现 GenericSeq 接口类型,也必须实现 Sortable 接口类型。

现在,将嵌入到 Sequence 类型的 Sortable 接口类型改为 GenericSeq 接口类型,声明如下:

type Sequence struct {
   
    GenericSeq
    sorted bool
    elemType reflect.Type
}

在如上的类型声明中,添加了一个 reflect.Type 类型(即标准库代码包 reflect 中的 Type 类型)的字段 elemType,目的用它来缓存 GenericSeq 字段中存储的值的元素类型。

为了能够在改变 GenericSeq 字段存储的值的过程中及时对字段 sortedelemType 的值进行修改,如下还创建了几个与 Sequence 类型关联的方法。声明如下:

func (self *Sequence) Sort() {
   
    self.GenericSeq.Sort()
    self.sorted = true
}

func (self *Sequence) Append(e interface{
   }) bool{
   
    result := self. GenericSeq.Append(e)
    //省略部分代码
    self.sorted = true
    //省略部分代码
    return result
}

func (self *Sequence) Set(index int, e interface{
   }) bool {
   
    result := self. GenericSeq.Set(index, e)
    //省略部分代码
    self.sorted = true
    //省略部分代码
    return result
}

func (self *Sequence) ElemType() reflect.Type {
   
    //省略部分代码
    self.elemType = self.GenericSeq.ElemType()
    //省略部分代码
    return self.elemType
}

如上的这些方法分别与接口类型 GenericSeqSortable 中声明的某个方法有着相同的方法名称和方法签名。通过这种方式隐藏了 GenericSeq 字段中存储的值的这些同名方法,并对它们进行了无缝扩展。

附录

GenericSeq 接口类型的实现类型以及 Sequence 类型的完整实现代码 点击这里

目录
相关文章
|
1天前
|
JSON 安全 Java
2024年的选择:为什么Go可能是理想的后端语言
【4月更文挑战第27天】Go语言在2024年成为后端开发的热门选择,其简洁设计、内置并发原语和强大工具链备受青睐。文章探讨了Go的设计哲学,如静态类型、垃圾回收和CSP并发模型,并介绍了使用Gin和Echo框架构建Web服务。Go的并发通过goroutines和channels实现,静态类型确保代码稳定性和安全性,快速编译速度利于迭代。Go广泛应用在云计算、微服务等领域,拥有丰富的生态系统和活跃社区,适合作为应对未来技术趋势的语言。
8 0
|
1天前
|
Go 开发者
Golang深入浅出之-Go语言项目构建工具:Makefile与go build
【4月更文挑战第27天】本文探讨了Go语言项目的构建方法,包括`go build`基本命令行工具和更灵活的`Makefile`自动化脚本。`go build`适合简单项目,能直接编译Go源码,但依赖管理可能混乱。通过设置`GOOS`和`GOARCH`可进行跨平台编译。`Makefile`适用于复杂构建流程,能定义多步骤任务,但编写较复杂。在选择构建方式时,应根据项目需求权衡,从`go build`起步,逐渐过渡到Makefile以实现更高效自动化。
9 2
|
1天前
|
存储 Go
Golang深入浅出之-Go语言依赖管理:GOPATH与Go Modules
【4月更文挑战第27天】Go语言依赖管理从`GOPATH`进化到Go Modules。`GOPATH`时代,项目结构混乱,可通过设置多个工作空间管理。Go Modules自Go 1.11起提供更现代的管理方式,通过`go.mod`文件控制依赖。常见问题包括忘记更新`go.mod`、处理本地依赖和模块私有化,可使用`go mod tidy`、`replace`语句和`go mod vendor`解决。理解并掌握Go Modules对现代Go开发至关重要。
7 2
|
1天前
|
安全 测试技术 Go
Golang深入浅出之-Go语言单元测试与基准测试:testing包详解
【4月更文挑战第27天】Go语言的`testing`包是单元测试和基准测试的核心,简化了测试流程并鼓励编写高质量测试代码。本文介绍了测试文件命名规范、常用断言方法,以及如何进行基准测试。同时,讨论了测试中常见的问题,如状态干扰、并发同步、依赖外部服务和测试覆盖率低,并提出了相应的避免策略,包括使用`t.Cleanup`、`t.Parallel()`、模拟对象和检查覆盖率。良好的测试实践能提升代码质量和项目稳定性。
7 1
|
1天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1
|
1天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
5 0
|
1天前
|
Go C++
go 语言回调函数和闭包
go 语言回调函数和闭包
|
1天前
|
存储 负载均衡 监控
【Go 语言专栏】构建高可靠性的 Go 语言服务架构
【4月更文挑战第30天】本文探讨了如何利用Go语言构建高可靠性的服务架构。Go语言凭借其高效、简洁和并发性能,在构建服务架构中备受青睐。关键要素包括负载均衡、容错机制、监控预警、数据存储和服务治理。文章详细阐述了实现这些要素的具体步骤,通过实际案例分析和应对挑战的策略,强调了Go语言在构建稳定服务中的作用,旨在为开发者提供指导。
|
1天前
|
测试技术 Go 开发工具
【Go语言专栏】Go语言中的代码审查与最佳实践
【4月更文挑战第30天】Go语言因其简洁、高性能及并发能力,在云计算等领域广泛应用。代码审查对提升Go代码质量、遵循规范及团队协作至关重要。审查流程包括提交、审查、反馈、修改和合并代码。工具如GoLand、Git、ReviewBoard和GitHub提供支持。最佳实践包括遵循命名规范、添加注释、保持代码结构清晰、复用代码和确保测试覆盖。积极参与代码审查是提高质量的关键。
|
1天前
|
Prometheus 监控 Cloud Native
【Go语言专栏】Go语言中的日志记录与监控
【4月更文挑战第30天】Go语言在软件开发和运维中扮演重要角色,提供灵活的日志记录机制和与多种监控工具的集成。内置`log`包支持基本日志记录,而第三方库如`zap`、`zerolog`和`logrus`则扩展了更多功能。监控方面,Go应用可与Prometheus、Grafana、Jaeger等工具配合,实现系统指标收集、可视化和分布式追踪。健康检查通过HTTP端点确保服务可用性。结合日志和监控,能有效提升Go应用的稳定性和性能。