写点什么

Go 学习笔记——复合数据结构之结构体

作者:为自己带盐
  • 2022 年 3 月 05 日
  • 本文字数:4032 字

    阅读完需:约 13 分钟

Go学习笔记——复合数据结构之结构体

书接上回《复合数据结构》

这一篇,捎带复习了一下之前的内容,因为我的笔记不是从最开始记录的,所以会有一些穿插。。。因为是在一边敢项目,一边学 go 所以笔记有点乱,进度也有点慢,现在还是在熟悉基础知识的阶段。

先补个漏

环境配置

近期我的机器硬件出问题了,经常蓝屏,就换了台电脑。随之而来的就是各种开发环境需要重新搭建,在搭建 go 开发环境的时候,遇到了一点小问题,这里记录一下。

正常来说,如果个人电脑是 Windows 系统,我还是比较推荐结合使用 wsl2+vscode 来使用的,等于是在 linux 内核的系统里安装 go 环境,然后通过 vscode 的远程开发插件,直接进行开发,非常好用,对于以后移植生产也会方便不少。但我们单位的机子非常辣鸡,开了 hyper-v 之后简直没法看,所以我就还是直接安装的 Windows 的 msi 包,传送门

我安装的是最新的稳定版,1.17.8,装好之后,用 vscode 装好 go 的扩展插件,然后,就开始突突冒问题了

什么“the gopls command is not available”之类的。然后 vscode 的左下标这里会出现一个叹号

我这里因为已经处理好了,所以变成了闪电⚡。

然后按照编辑器的提示,一致没装好,提示超时之类的问题,开始我以为是我的 go 版本太新了,导致有的插件没有安装成功,后来我试了低版本的稳定版,依然不行,所以考虑还是网络问题。这里,就其实处理起来也简单,就全局把 go 环境的下载包源改一下就好了,建议直接使用七牛云的模块代理,这个地址里也有具体用法,我这里在简单引用一下;

Windows 环境

在 powershell 里输入

$env:GO111MODULE = "on"$env:GOPROXY = "https://goproxy.cn"
复制代码

linux&Mac

$ export GO111MODULE=on$ export GOPROXY=https://goproxy.cn
复制代码

或者

$ echo "export GO111MODULE=on" >> ~/.profile$ echo "export GOPROXY=https://goproxy.cn" >> ~/.profile$ source ~/.profile
复制代码

设置好以后,再安装 go 的开发模块,就会顺畅许多了(可能要重开一下 vscode,或者直接在 windows Terminal 里安装)

gomodule 模式

在 go 1.15 以后,go 默认的构建模式就是 module 模式了,而早期的 gopah 模式将会被移除

我这里再练习结构体的时候,发现自己定义的结构体要引入 main 文件时,一直出错,而我一直是在用的 gopath 模式,如果用这种模式的话,自己定义的包,需要放到 gopath 目录下的 src 下才能引入。改用 go module 模式后,只需要通过把包放到项目下即可。这里就以一个例子来看


比如我要引入自定义的 book 里的内容,则可以这样


package store
type Person struct { Name string Phone string Addr string _ int}
type Book struct { Title string Person Indexes map[string]int Pages int}
复制代码

然后引入的时候,就这样

package main
import ( "fmt" "go17/internal/store")...
复制代码

然后引入第三方的包时,使用 go mod tidy 命令来进行构建。好了具体的内容不再多说,可以参见这里


正题开始

概念

go 语言中的结构体,是通过整合多种基本数据类型和符合数据类型,来构建对真实世界的抽象,而提供这种聚合抽象能力的类型,就是结构体类型(struct)。我感觉可以理解为 Json

再回头看一下怎么定义新类型

方法一、类型定义

type T S //定义一个新类型T
复制代码

注意,上面的伪代码里,S 表示一个已定义的类型或者基础类型,比如以下两种情况

type T1 inttype T2 T1
复制代码

这里引入一个底层类型的概念,比如上面的例子,类型 T1 的底层类型是 int,而 T2 的是基于 T1 创建的,所以 T2 的底层类型也是 int,底层类型在 go 语言中被作为判定两个类型本质上是否相同,只有本质上相同的两个类型,其变量才可以通过显示转型进行相互赋值

//比如这段代码type T1 inttype T2 T1type T3 stringfunc main() {    var n1 T1    var n2 T2 = 5    n1 = T1(n2)  // ok        var s T3 = "hello"    n1 = T1(s) // 错误:cannot convert s (type T3) to type T1}
复制代码

类型定义和变量声明相似,也可以放入块中进行

type {    T1 int    T2 T1    T3 string}
复制代码

类型定义也支持通过类型字面值来定义新类型,我觉得这种方式在实际项目中会经常使用,是一个显著降低代码量的方式比如

type M map[int]stringtype S []string
复制代码

方法二、类型别名

别名定义的特点就是通过“=”链接两个类型,等号两端的类型是完全一致的,只是名字不同。

type T = S // type alias
复制代码

这个比较容易理解,直接看个具体的例子

func main() {    type T = string    s := "hello"    var t T = s    fmt.Printf("t的类型是【%T】\ns的类型是【%T】\n", t, s)}
复制代码


如何定义一个结构体类型?


类型字面值

复合类型的定义一般都是通过类型字面值的方式来进行的,作为复合类型之一的结构体类型也不例外


type T struct {    Field1 T1    Field2 T2    ... ...    FieldN Tn}
复制代码


struct 关键字后面的大括号所包裹的内容,就是一个类型字面值。(这看起来好像有点像 map,但它比 map 更加抽象,且 map 类型可以作为结构体中的字段)通过这种聚合其他类型字段,结构体类型展现出了灵活的抽象能力举个实例比如通过结构体来展示现实世界中的书,书的定义包含了作者,书名,页码等等信息,通过结构体可以这样来定义

type Person struct {	Name  string	Phone string	Addr  string	_     int //隐藏字段}
type Book struct { Title string Person Indexes map[string]int Pages int}
复制代码

这里要再提一下,结构体类型中的字段首字母都是用的大写,目的是标识该模块各个字段都是导出标识符,也就是只要其他包引入了 book 包,就可以在这些包中直接引用类型名 Book,也可以通过 Book 变量来直接引用 Name,Pages 等字段(和 C#里定义模型类类似)

package main
import ( "fmt" "go17/internal/store")
func main() { var book store.Book fmt.Println(book.Person.Phone)//nil book.Person.Phone = "110" fmt.Println(book.Person.Phone)//110}
复制代码

而如果结构体类型旨在定义的包内使用,就可以将类型名的首字母小写,或者只是不想结构体中某个字段爆露出来,则只需要把对应的字段首字母小写



像这样,把 Phone 字段首字母小写,编辑器就会报错了,改成小写也不行


空结构体

空结构体也是定义结构体的一种方法,它没有包含任何字段,通过 unsafe 包获取它的大小也会得到一个 0,也就是无内存占用,

type Empty struct{} // Empty是一个不包含任何字段的空结构体类型
复制代码



var s Emptyprintln(unsafe.Sizeof(s)) // 0
复制代码

而这里的实际意义,课上老师说的是作为“事件”信息进行 Goroutine 之间通信,具体的内容我就不罗列了,对我来说现阶段再往深里挖会增加心智负担,所以我决定先跳过这里,先知道有空结构体这个概念就好。


使用其他结构体作为自定义结构体中字段的类型

我在上面的例子其实就是这种情况,Book 里的 Author 字段,其实是另外引用的一个 Person 的结构体,这个和 C#语言里的自定义模型类也很类似,所以不难理解,为了语法的简洁,甚至可以取消变量名,比如

type Book struct {  Title string  Author Person}//可以直接改写成-----type Book struct {  Title string  Person}
复制代码

而在实际使用时,直接调用 book.Person.xx 就可以了,这种方式叫做“嵌入字段”或者匿名字段,也就是类型名本身也可以当作变量名

但是注意,以下这些情况是不被允许的

type T struct {    t T      ... ...}
type T1 struct { t2 T2}
type T2 struct { t1 T1}
复制代码

Go 语言不支持这种在结构体类型定义中,递归地放入其自身类型字段的定义方式。但我们却可以拥有自身类型的指针类型、以自身类型为元素类型的切片类型,以及以自身类型作为 value 类型的 map 类型的字段

,比如这样


type T struct { t *T // ok st []T // ok m map[string]T // ok}
复制代码

这里的原因在课程里关于结构体内存结构分析的部分有详细介绍,我在后边也不多说了,就直接说一下为啥可以,因为结构体本身是一种高度抽象的数据类型,抽象包含抽象的话,编译器无法得到内部字段的具体类型,也就无法划分内存空间,所以是无法编译通过的,而以自身类型的指针,切片和 map 类型,是可以划分内存空间的,所以是可以编译通过的

结构体变量的声明与初始化

和其他所有变量的声明一样,我们也可以使用标准变量声明语句,或者是短变量声明语句声明一个结构体类型的变量

ps.这里的下划线报警时因为这几个变量我只是定义了,却没有使用,实际编译是可以通过的。

不过,这里要注意,我们在前面说过,结构体类型通常是对真实世界复杂事物的抽象,这和简单的数值、字符串、数组 / 切片等类型有所不同,结构体类型的变量通常都要被赋予适当的初始值后,才会有合理的意义


变量初始化的方式大致分为三种

零值初始化

结构体类型本身是“零值可用”的,也就是我们定义好结构体后,其内部各个字段的值都是对应的零值状态。

使用复合字面值

最简单的对结构体变量进行显式初始化的方式,就是按顺序依次给每个结构体字段进行赋值,比如下面的代码

//注意,这么写虽然正确,但这是反例奥~//这种情况当遇到结构体内字段较多的情况后,在进行初始化,就很麻烦了,go也不推荐我们这么做type Book struct {    Title string              // 书名    Pages int                 // 书的页数    Indexes map[string]int    // 书的索引}
var book = Book{"The Go Programming Language", 700, make(map[string]int)}
复制代码


Go 推荐我们用“field:value”形式的复合字面值,对结构体类型变量进行显式初始化(形式上有点像 map 或者哈希结构)

var t = T{    F2: "hello",    F1: 11,    F4: 14,}
复制代码

使用特定的构造函数


好了,关于这篇的笔记就记录到这里,还有结构体类型的内存布局的部分我没有记录,但这块我是看了的,也基本明白了,但我条过记录还是觉得这部分的记录会增加心智负担,但注意,这并不是不想学习的的原因,只是暂时不做记录,为的是后续我再返回来看的时候,会很容易消化掉比较容易消化的部分,而偏原理性的内容,我会先认真看一遍,然后先明白大概意思就达成目标了,后续真正用 go 做项目的时候,肯定还是会重新深入这部分。先这样啦

发布于: 刚刚阅读数: 3
用户头像

学着码代码,学着码人生。 2019.04.11 加入

狂奔的小码农

评论

发布
暂无评论
Go学习笔记——复合数据结构之结构体_Go_为自己带盐_InfoQ写作平台