Go基础篇

Go命令行工具

// 当前路径编译
go build
go build -o 新名字.exe
// 编译后可执行文件保留在命令执行的路径
go build 完整项目名
// 运行 go 文件
go run main.go
// 编译后移动可执行文件到 GOPATH/bin 目录
go install
// 查看go环境变量
go env
// 修改环境变量,direct 是一个特殊指示符,用于指示 Go 回到源地址去抓取(比如 GitHub 等)
go env -w GOPROXY=https://goproxy.cn,direct
go env -w GO111MODULE=on
// 初始化项目,完整项目名 通常设置为:域名/作者/项目名
go mod init 完整项目名
// 从网上下载源码到 $GOPATH/src/,-u表示强制更新现有依赖
go get -u gopl.io/ch1/helloworld
// 下载当前目录中go.mod所指定的依赖包
go mod download

跨平台编译

Windows编译可执行文件

cmd

PowerShell

再执行下面的命令,得到能够在Linux平台运行的可执行文件

Mac编译可执行文件

Linux编译可执行文件

VSCode中配置Go

语法特性

  • 语句末尾不加分号

  • 变量声明时会自动赋予默认值

    • 整型和浮点型=>0

    • 字符串=>""

    • 布尔型=>false

    • 切片、函数、指针变量=>nil

  • 类型推导,在声明变量时可以自动判断变量类型,而不用声明变量类型

  • 短变量声明::=,只能用于函数体内部

  • 匿名变量声明:_,表示占位,用于接收被忽略的值

  • 函数体外部的每个语句都必须以关键字开始(var、const、func等)

  • 不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换

  • 字符串只能使用双引号 "" 和反引号 ` ` 而不能使用单引号 ''

  • 反引号用于定义多行的字符串,多行字符串中的转义字符将失效

  • 字符串拼接使用 +

变量

常量

只需把 var 换成了 const,常量在定义的时候必须赋值

位移运算

<< 表示左移操作,1<<10 表示将 1 的二进制表示向左移10位,也就是由 1 变成了 10000000000 ,也就是十进制的 1024

数据类型

内置函数

Number literals syntax

v := 0b00101101, 代表二进制的 101101,相当于十进制的 45v := 0o377,代表八进制的 377,相当于十进制的 255v := 0x1p-2,代表十六进制的 1 除以 ,也就是 0.25

浮点型

byte和rune类型

  • uint8 类型,或者叫 byte 型,代表了ASCII码 的一个字符

  • rune 类型,代表一个 UTF-8字符

  • 当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。

  • rune 类型实际是一个 int32

字符串

要修改字符串,需要先将其转换成 []rune[]byte ,完成后再转换为 string。无论哪种转换,都会重新分配内存,并复制字节数组

判断结构

循环结构

for range 循环可以遍历数组、切片、字符串、map 及 通道(channel)

Switch Case

Break

Continue

Array

  • 定义:var name [元素数量]Type

  • 数组的长度必须是常量,并且长度是数组类型的一部分,长度不同的数组类型也不同,数组一旦定义,长度就不能改变

  • 多维数组只有第一层可以使用 ... 进行数组长度推导

  • 数组是值类型,赋值和传参会复制整个数组,因此改变副本的值,不会改变本身的值

  • [n]*T 表示指针数组,*[n]T 表示数组指针

初始化数组

一维

多维

遍历数组

一维

多维

切片

切片是一个引用类型,它的内部结构包含 地址长度容量

声明:var name []Type

初始化:var name []Type{}

内置的 len() 函数求切片的长度,内置的 cap() 函数求切片的容量

简单表达式

完整表达式

表达式为 a[low:high:max],最终得到的是一个长度是 high - low,容量为 max - low 的切片

make()函数

定义:make([]Type, size, cap)

要检查切片是否为空,请始终使用 len(a) == 0 来判断,而不应该使用 a == nil 来判断

赋值拷贝

切片是引用类型,都指向了同一块内存地址,赋值拷贝后两个变量共享底层数组

copy()函数

定义:copy(destSlice, srcSlice []Type)

  • srcSlice: 数据来源切片

  • destSlice: 目标切片

遍历切片

append()函数

切片扩容策略

删除元素

从切片a中删除索引为index的元素:a = append(a[:index], a[index+1:]...)

区分数组与切片

var arr [5]int

slice := make([]int,3,5)

map

定义:make(map[KeyType]ValueType, [cap])

遍历map

遍历map时的元素顺序与添加键值对的顺序无关

delete()函数

定义:delete(map, key)

指定顺序遍历map

切片中存map

map类型的切片

map中存切片

切片类型的map

函数

定义

返回值

变量作用域

全局变量

局部变量

变量优先级

如果局部变量和全局变量重名,优先访问局部变量

函数类型与变量

高阶函数

函数作为参数

函数作为返回值

匿名函数

匿名函数需要保存到某个变量或者作为立即执行函数,多用于实现回调函数和闭包

闭包

闭包=函数+引用环境

defer语句

defer 后面跟随的语句进行延迟处理,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。由于 defer 语句延迟调用的特性,所以 defer 语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。

输出:

defer执行时机

在Go语言的函数中 return 语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而 defer 语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:

defer经典案例

defer面试题

defer注册要延迟执行的函数时,该函数所有的参数都需要确定其值

内置函数

内置函数
介绍

close

主要用来关闭channel

len

用来求长度,比如string、array、slice、map、channel

new

用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针

make

用来分配内存,主要用来分配引用类型,比如chan、map、slice

append

用来追加元素到数组、slice中

panic和recover

用来做错误处理

异常处理

panic 可以在任何地方引发,但 recover 只有在 defer 调用的函数中有效

程序运行期间 funcB 中引发了 panic 导致程序崩溃,异常退出了。这个时候我们就可以通过 recover 将程序恢复回来,继续往后执行

注意:

  1. recover() 必须搭配 defer 使用。

  2. defer 一定要在可能引发 panic 的语句之前定义。

指针

指针取值

输出

**总结:**取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。

  • 指针变量的值是指针地址。

  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。

指针传值

内存分配

执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。Go语言中new和make是内建的两个函数,主要用来分配内存。

new

new是一个内置的函数,它的函数签名如下:

  • Type表示类型,new函数只接受一个参数,这个参数是一个类型

  • *Type表示类型指针,new函数返回一个指向该类型内存地址的指针

new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值

开始的示例代码中 var a *int 只是声明了一个指针变量a但是没有初始化,指针作为引用类型需要初始化后才会拥有内存空间,才可以给它赋值。应该按照如下方式使用内置的new函数对a进行初始化之后就可以正常对其赋值

make

make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作

开始的示例中var b map[string]int只是声明变量b是一个map类型的变量,需要像下面的示例代码一样使用make函数进行初始化操作之后,才能对其进行键值对赋值

new与make的区别

  1. 二者都是用来做内存分配的

  2. make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身

  3. 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针

type

自定义类型

类型别名

区别

结果显示

a(自定义)类型是 main.NewInt,表示main包下定义的 NewInt 类型。

b(别名)类型是 int,MyInt 类型只会在代码中存在,编译完成时并不会有 MyInt 类型。

结构体

Go语言中通过 struct 来实现面向对象

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。

  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。

  • 字段类型:表示结构体字段的具体类型。

结构体实例化

注:

匿名结构体

指针结构体

Go语言中支持对结构体指针直接使用 . 来访问结构体的成员

取结构体的地址实例化

使用 & 对结构体进行取地址操作,相当于对该结构体类型进行了一次 new 实例化操作

p.name = "张三" 其实在底层是 (*p).name = "张三",这是Go语言帮我们实现的语法糖

结构体初始化

没有初始化的结构体,其成员变量都是对应其类型的零值

结构体内存布局

结构体占用一块连续的内存

空结构体

空结构体是不占用空间的

面试题

构造函数

Go语言的结构体没有构造函数,可以自己实现。 例如,下方的代码就实现了一个 person 的构造函数。 因为 struct 是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

方法和接收者

Go语言中的 方法(Method) 是一种作用于特定类型变量的函数。这种特定类型变量叫做 接收者(Receiver)。接收者的概念就类似于其他语言中的 this 或者 self

方法的定义格式如下:

其中

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称的首字母小写,而不是 selfthis 之类的命名。例如,Person 类型的接收者变量应该命名为 pConnector 类型的接收者变量应该命名为 c

  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型

  • 方法名、参数列表、返回参数:具体格式与函数定义相同

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型

指针类型的接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self。 例如我们为Person添加一个SetAge方法,来修改实例变量的年龄

值类型的接收者

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身

什么时候应该使用指针类型接收者

  1. 需要修改接收者中的值

  2. 接收者是拷贝代价比较大的大对象

  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者

任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法

注意:非本地类型不能定义方法,也就是说我们不能给别的包的类型定义方法

结构体的匿名字段

结构体允许其成员字段在声明时没有字段名而只有类型,这种没有名字的字段就称为匿名字段

注意:这里匿名字段的说法并不代表没有字段名,而是默认会采用类型名作为字段名,结构体要求字段名称必须唯一,因此一个结构体中同种类型的匿名字段只能有一个

嵌套结构体

一个结构体中可以嵌套包含另一个结构体或结构体指针

嵌套匿名字段

上面user结构体中嵌套的 Address 结构体也可以采用匿名字段的方式

当访问结构体成员时会先在结构体中查找该字段,找不到再去嵌套的匿名字段中查找

嵌套结构体的字段名冲突

嵌套结构体内部可能存在相同的字段名,在这种情况下为了避免歧义需要通过指定具体的内嵌结构体字段名

结构体的“继承”

Go语言中使用结构体也可以实现其他编程语言中面向对象的继承

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)

结构体与JSON序列化

结构体标签(Tag)

Tag 是结构体的元信息,可以在运行的时候通过反射的机制读取出来。Tag 在结构体字段的后方定义,由一对反引号包裹起来

结构体tag由一个或多个键值对组成。键与值使用冒号分隔,值用双引号括起来。同一个结构体字段可以设置多个键值对tag,不同的键值对之间使用空格分隔。

注意:为结构体编写 Tag 时,必须严格遵守键值对的规则。结构体标签的解析代码的容错能力很差,一旦格式写错,编译和运行时都不会提示任何错误,通过反射也无法正确取值。例如不要在key和value之间添加空格。

结构体和方法

因为slice和map这两种数据类型都包含了指向底层数据的指针,因此我们在需要复制它们时要特别注意

正确的做法是在方法中使用传入的slice的拷贝进行结构体赋值

同样的问题也存在于返回值slice和map的情况,在实际编码过程中一定要注意这个问题

创建包

在Go语言中通过标识符的首字母 大/小写 来控制标识符的对外 可见(public)/不可见(private)

引入包

如果引入一个包的时候为其设置了一个特殊 _ 作为包名,那么这个包的引入方式就称为匿名引入。一个包被匿名引入的目的主要是为了加载这个包,从而使得这个包中的资源得以初始化。被匿名引入的包中的 init 函数将被执行并且仅执行一遍。匿名引入的包与其他方式导入的包一样都会被编译到可执行文件中。

init()函数

init()函数不接收任何参数也没有任何返回值,我们也不能在代码中主动调用它。当程序启动的时候,init函数会按照它们声明的顺序自动执行。一个包的初始化过程是按照代码中引入的顺序来进行的,所有在该包中声明的 init 函数都将被串行调用并且仅调用执行一次。每一个包初始化的时候都是先执行依赖的包中声明的 init 函数再执行当前包中声明的 init 函数。确保在程序的 main 函数开始执行时所有的依赖包都已初始化完成。

每一个包的初始化是先从初始化包级别变量开始的。例如从下面的示例中我们就可以看出包级别变量的初始化会先于 init 初始化函数。

go module

相关命令

命令
介绍

go mod init

初始化项目依赖,生成go.mod文件

go mod download

根据go.mod文件下载依赖

go mod tidy

比对项目文件中引入的依赖与go.mod进行比对

go mod graph

输出依赖关系图

go mod edit

编辑go.mod文件

go mod vendor

将项目的所有依赖导出至vendor目录

go mod verify

检验一个依赖包是否被篡改过

go mod why

解释为什么需要某个依赖

GOPRIVATE

设置了GOPROXY 之后,go 命令就会从配置的代理地址拉取和校验依赖包。当我们在项目中引入了非公开的包(公司内部git仓库或 github 私有仓库等),此时便无法正常从代理拉取到这些非公开的依赖包,这个时候就需要配置 GOPRIVATE 环境变量。GOPRIVATE用来告诉 go 命令哪些仓库属于私有仓库,不必通过代理服务器拉取和校验。GOPRIVATE 的值也可以设置多个,多个地址之间使用英文逗号 “,” 分隔。我们通常会把自己公司内部的代码仓库设置到 GOPRIVATE 中,例如:

这样在拉取以 git.mycompany.com 为路径前缀的依赖包时就能正常拉取了。此外,如果公司内部自建了 GOPROXY 服务,那么我们可以通过设置 GONOPROXY=none,允许通内部代理拉取私有仓库的包。

go.mod文件

go.mod 文件中记录了当前项目中所有依赖包的相关信息,声明依赖的格式如下:

其中:

  • require:声明依赖的关键字

  • module/path:依赖包的引入路径

  • v1.2.3:依赖包的版本号。支持以下几种格式:

    • latest:最新版本

    • v1.0.0:详细版本号

    • commit hash:指定某次commit hash

引入某些没有发布过 tag 版本标识的依赖包时,go.mod 中记录的依赖版本信息就会出现类似 v0.0.0-20210218074646-139b0bcd549d 的格式,由版本号、commit时间和commit的hash值组成。

go.sum文件

使用go module下载了依赖后,项目目录下还会生成一个go.sum文件,这个文件中详细记录了当前项目中引入的依赖包的信息及其hash 值。go.sum 文件内容通常是以类似下面的格式出现。

或者

不同于其他语言提供的基于中心的包管理机制,例如 npm 和 pypi 等,Go并没有提供一个中央仓库来管理所有依赖包,而是采用分布式的方式来管理包。为了防止依赖包被非法篡改,Go module 引入了 go.sum 机制来对依赖包进行校验。

依赖保存位置

Go module 会把下载到本地的依赖包以类似下面的形式保存在 $GOPATH/pkg/mod 目录下,每个依赖包都会带有版本号进行区分,这样就允许在本地存在同一个包的多个不同版本。

如果想清除所有本地已缓存的依赖包数据,可以执行 go clean -modcache 命令。

发布包

在github创建一个名为hello的项目

创建一个 hello.go 文件

打好 tag 推送到远程仓库

经过上面的操作我们就发布了一个版本号为 v0.1.0 的版本。

Go modules中建议使用语义化版本控制,其建议的版本号格式如下:

其中:

  • 主版本号:发布了不兼容的版本迭代时递增(breaking changes)。

  • 次版本号:发布了功能性更新时递增。

  • 修订号:发布了bug修复类更新时递增。

发布新的主版本

现在我们的 hello 项目要进行与之前版本存在不兼容的更新,我们计划让 SayHi 函数支持向指定人发出问候。更新后的 SayHi 函数内容如下:

由于改动巨大(修改了函数之前的调用规则),对之前使用该包作为依赖的用户影响巨大,需要发布一个主版本号递增的 v2 版本

把修改后的代码提交

打好 tag 推送到远程仓库

这样在不影响使用旧版本的用户的前提下,我们新的版本也发布出去了。想要使用 v2 版本的代码包的用户只需按修改后的引入路径下载即可

在代码中使用的过程与之前类似,只是需要注意引入路径要添加 v2 版本后缀。

废弃已发布版本

如果某个发布的版本存在致命缺陷不再想让用户使用时,我们可以使用 retract 声明废弃的版本。例如我们在 hello/go.mod 文件中按如下方式声明即可对外废弃 v0.1.2 版本

用户使用go get下载 v0.1.2 版本时就会收到提示,催促其升级到其他版本

Last updated