Bootstrap

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 int
type T2 T1

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

//比如这段代码
type T1 int
type T2 T1
type T3 string
​
func 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]string
type 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 Empty
println(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做项目的时候,肯定还是会重新深入这部分。先这样啦