决战Go语言从入门到入土v0.1

在这里插入图片描述
下载地址:https://gitcode.net/as604049322/blog_pdf

安装与运行环境

Go 语言环境安装

Go语言支持Linux、Mac和Windows,本人接下来的学习全部基于windows电脑进行操作。

Go官方镜像站点:https://golang.google.cn/dl/

关于GO语言的版本,选择默认的最高版本就好,Go代码向下兼容,版本之间的差异并无所谓。

作为windows系统的我下载了下面这个包:

image-20211126230701626

ARM64是ARM中64位体系结构,x64是x86系列中的64位体系。ARM属于精简指令集体系,汇编指令比较简单,比如晓龙的CPU,华为麒麟的CPU等等。

打开安装包,我安装到了D:\deploy\go\的位置。

安装完成后,查看是否安装成功

>go version
go version go1.17.3 windows/amd64

输入go env查看Go配置:

>go env
...
set GOPROXY=https://proxy.golang.org,direct
set GOROOT=D:\deploy\go
...

可能我们需要下载Go的一些第三方包,但是默认官网源GOPROXY=https://proxy.golang.org,direct,在国内访问不到。

可以改成国内的七牛云镜像站点:

go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct

运行环境runtime:

Go 编译器产生的是本地可执行代码会将 runtime 嵌入其中,这些代码仍运行在 Go 的 runtime 当中。这个 runtime 类似 Java 和 .NET 语言所用到的虚拟机,它负责管理包括内存分配、垃圾回收、栈处理、goroutine、channel、切片(slice)、map 和反射(reflection)等等。

runtime 主要由 C 语言编写(Go 1.5 开始自举),并且是每个 Go 包的最顶级包。可以在目录 $GOROOT/src/runtime 中找到相关内容。

Go 拥有简单却高效的标记-清除的垃圾回收器。

常用命令

构建并运行 Go 程序主要是以下三个命令:

  • go build 编译并安装自身包和依赖包
  • go install 安装自身包和依赖包
  • go run 编译并运行

go语言包含了格式化源码的工具gofmtgofmt –w program.go 会格式化该源文件的代码然后将格式化后的代码覆盖原始内容(如果不加参数 -w则只会打印格式化后的结果而不重写文件)

效果如下:

image-20211202122637737

go doc 工具会从 Go 程序和包文件中提取顶级声明的首行注释以及每个对象的相关注释,并生成相关文档。

  • go doc package 获取包的文档注释,例如:go doc fmt 会显示使用 godoc 生成的 fmt 包的文档注释。
  • go doc package/subpackage 获取子包的文档注释,例如:go doc container/list
  • go doc package function 获取某个函数在某个包中的文档注释,例如:go doc fmt Printf 会显示有关fmt.Printf() 的使用说明。

更多有关 godoc 的信息:http://golang.org/cmd/godoc/(在线版的第三方包 godoc 可以使用 Go Walker

Goland开发工具安装

开发 Python 项目,很多人习惯了 风格的PyCharm。Goland则是 JetBrains 风格的Go语言开发工具。

首先到https://www.jetbrains.com/zh-cn/go/download/other.html选择一个合适的版本下载,这里我下载了https://download.jetbrains.com/go/goland-2019.3.1.exe

下载后打开安装包,一路Next,先选择安装路径,再选择安装选项:

image-20211209145834764

个人只选择了创建快捷方式,等待2分钟安装完毕。

Go语言的三个重要演进

演进一:Go 1.4 版本删除 pkg 这一中间层目录并引入 internal 目录

出于简化源码树层次的原因,Go 1.4 版本删除了 Go 源码树**“src/pkg/xxx”**中的 pkg 而直接使用 “src/xxx”

1.4 引入 internal 包机制,增加了 internal 目录。**internal 机制的定义:**一个 Go 项目里的 internal 目录下的 Go 包,只可以被本项目内部的包导入。项目外部是无法导入这个 internal 目录下面的包的。

演进二:Go1.6 版本增加 vendor 目录

Go 核心团队为了解决 Go 包依赖版本管理的问题,在 Go 1.5 版本中增加了 vendor 构建机制,也就是 Go 源码的编译可以不在 GOPATH 环境变量下面搜索依赖包的路径,而在 vendor 目录下查找对应的依赖包。

不过在 Go 1.6 版本中 vendor 目录并没有实质性缓存任何第三方包。直到 Go 1.7 版本,Go 才真正在 vendor 下缓存了其依赖的外部包。

演进三:Go 1.13 版本引入 go.mod 和 go.sum

这次演进依然是为了解决 Go 包依赖版本管理的问题。在 Go 1.11 版本中,Go 核心团队引入了 Go Module 构建机制,即在 go.mod 中明确项目所依赖的第三方包和版本,项目的构建从此摆脱 GOPATH 的束缚实现精准的可重现构建。

Go 1.13 版本 Go 语言项目自身的 go.mod 文件内容:

module std
go 1.13
require (
  golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8
  golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7
  golang.org/x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect
  golang.org/x/text v0.3.2 // indirect
)

可以看到,Go 语言项目自身所依赖的包在 go.mod 中都有对应的信息,而原本这些依赖包是缓存在 vendor 目录下的。

Go项目的布局标准

Go 可执行程序项目的典型结构布局:

exe-layout
├── cmd/
│   ├── app1/
│   │   └── main.go
│   └── app2/
│       └── main.go
├── go.mod
├── go.sum
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
├── pkg2/
│   └── pkg2.go
└── vendor/

cmd 目录存放项目要编译构建的可执行文件对应的 main 包的源文件,每个可执行文件的 main 包单独放在一个子目录中。

pkgN 目录存放项目自身依赖的库文件,同时这些目录下的包还可以被外部项目引用。

go.modgo.sum是 Go 语言包依赖管理使用的配置文件,这是目前 Go 官方推荐的标准构建模式。

在 Go Modules 机制引入前,基于 vendor 可以实现可重现构建,保证基于同一源码构建出的可执行程序是等价的。Go Module出现后,vendor 目录 作为一个可选目录被保留下来,通过 go mod vendor 可以生成 vendor 下的依赖包,通过 go build -mod=vendor 可以实现基于 vendor 的构建。一般我们仅保留项目根目录下的 vendor 目录,否则会造成不必要的依赖选择的复杂性。

当然很多早期接纳Go语言的开发者可能会将原本放在项目顶层目录下的 pkg1 和 pkg2 公共包被统一聚合到 pkg 目录:

early-project-layout
└── exe-layout/
    ├── cmd/
    │   ├── app1/
    │   └── app2/
    ├── go.mod
    ├── internal/
    │   ├── pkga/
    │   └── pkgb/
    ├── pkg/
    │   ├── pkg1/
    │   └── pkg2/
    └── vendor/

Go Modules 支持在一个代码仓库中存放多个 module,例如:

multi-modules
├── go.mod // mainmodule
├── module1
│   └── go.mod // module1
└── module2
    └── go.mod // module2

可以通过 git tag 名字来区分不同 module 的版本。其中 vX.Y.Z 形式的 tag 名字用于代码仓库下的 mainmodule;而 module1/vX.Y.Z 形式的 tag 名字用于指示 module1 的版本;同理,module2/vX.Y.Z 形式的 tag 名字用于指示 module2 版本。

只有一个可执行程序要构建结构布局:

single-exe-layout
├── go.mod
├── internal/
├── main.go
├── pkg1/
├── pkg2/
└── vendor/

删除了 cmd 目录,将唯一的可执行程序的 main 包就放置在项目根目录下,而其他布局元素的功用不变。

仅对外暴露 Go 包的库类型项目的项目布局:

lib-layout
├── go.mod
├── internal/
│   ├── pkga/
│   │   └── pkg_a.go
│   └── pkgb/
│       └── pkg_b.go
├── pkg1/
│   └── pkg1.go
└── pkg2/
    └── pkg2.go

库类型项目不需要构建可执行程序,所以去除了 cmd 目录。另外,库项目通过 go.mod 文件明确表述出该项目依赖的 module 或包以及版本要求就可以了,vendor 不再是可选目录。

对于仅限项目内部使用而不想暴露到外部的包,可以放在 internal 目录下面。 internal 可以有多个并存在于项目结构中的任一目录层级中,关键是项目结构设计人员要明确各级 internal 包的应用层次和范围。

最简化的布局:

对于有一个且仅有一个包的 Go 库项目可以作如下简化:

single-pkg-lib-layout
├── feature1.go
├── feature2.go
├── go.mod
└── internal/

Go语言基础入门

推荐一个在线网站:https://tour.go-zh.org/list

Go 代码中会使用到的 25 个关键字或保留字:

breakdefaultfuncinterface
casedefergomap
chanelsegotopackage
constfallthroughifrange
continueforimportreturn

Go 语言的 36 个预定义标识符:

appendboolbytecapclosecomplex
copyfalsefloat32float64imagint
int32int64iotalenmakenew
printprintlnrealrecoverstringtrue

hello word

编写go源码文件hello.go,内容如下:

package main

import "fmt"

func main() {
    fmt.Println("Hello, World!")
}

然后再控制台执行:

E:\go_project>go run hello.go
Hello, World!

还可以使用 go build 命令来生成二进制文件:

E:\go_project>go build hello.go

E:\go_project>hello.exe
Hello, World!

注意: { 不能单独放在一行。例如以下代码会报错:

func main()  
{  // 错误,{ 不能在单独的行上
 fmt.Println("Hello, World!")
}

每个 Go 文件都属于且仅属于一个包。一个包可以由许多以 .go 为扩展名的源文件组成。

必须在源文件中非注释的第一行指明这个文件属于哪个包,如:package main表示一个可独立执行的程序,每个 Go 应用程序仅允许存在一个名为 main 的包,main 函数则是每个 Go 应用程序的入口函数。

import "fmt" 告诉 Go 编译器这个程序需要使用 fmt 包(的函数,或其他元素),fmt 包实现了格式化 IO(输入/输出)的函数。包名被封闭在半角双引号 "" 中。

如果需要多个包,它们可以被分别导入:

import "fmt"
import "os"

或:

import "fmt"; import "os"

但是还有更短且更优雅的方法(被称为因式分解关键字,该方法同样适用于 const、var 和 type 的声明或定义):

import (
   "fmt"
   "os"
)

它甚至还可以更短的形式,但使用 gofmt 代码格式化后将会被强制换行:

import ("fmt"; "os")

包的别名:

package main

import fm "fmt"

func main() {
	fm.Println("hello, world!")
}

Go 程序的启动顺序如下:

  1. 按顺序导入所有被 main 包引用的其它包,然后在每个包中执行如下流程:
  2. 如果该包又导入了其它的包,则从第一步开始递归执行,但是每个包只会被导入一次。
  3. 然后以相反的顺序在每个包中初始化常量和变量,如果该包含有 init 函数的话,则调用该函数。
  4. 在完成这一切之后,main 也执行同样的过程,最后调用 main 函数开始执行程序。

init 函数是Go的初始化函数,在main 函数之前,常量和变量初始化之后执行。和 main.main 函数一样,init 函数也是一个无参数无返回值的函数:

func init() {
    // 包初始化逻辑
    ... ...
}

在 Go 程序中我们不能手工显式地调用 init,否则就会收到编译错误,显示init没有被定义,例如:

package main
import "fmt"
func init() {
  fmt.Println("init invoked")
}
func main() {
   init()
}

报错信息为:undefined: init

每个组成 Go 包的 Go 源文件中可以定义多个 init 函数。在初始化时,Go 会按照一定的次序,逐一、顺序地调用这个包的 init 函数。同一个源文件中的多个 init 函数,会按声明顺序依次执行。

Go 包的初始化次序主要有三点:

  1. 依赖包按“深度优先”的次序进行初始化;
  2. 每个包内按以“常量 -> 变量 -> init 函数”的顺序进行初始化;
  3. 包内的多个 init 函数按出现次序进行自动调用。

Go语言的构建模式

Go 语言的构建模式历经了三个迭代和演化过程,分别是最初期的 GOPATH、1.5 版本的 Vendor 机制,以及现在的 Go Module。

GOPATH环境变量

Go语言的三个环境变量:

  • GOROOT:GO 语言的安装路径。
  • GOPATH:若干工作区目录的路径,自定义的工作空间。
  • GOBIN:GO 程序生成的可执行文件(executable file)的路径。

GOPATH 可以设置多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。这些工作区可以放置 Go 语言的源码文件(source file),以及安装(install)后的归档文件(archive file,也就是以“.a”为扩展名的文件)和可执行文件(executable file)。

**Go 语言源码的组织方式:**Go 语言的源码也是以代码包为基本组织单位的,与目录一一对应,子目录相当于子包。一个代码包中可以包含任意个以.go 为扩展名的源码文件,这些源码文件都需要被声明属于同一个代码包。

代码包的名称一般会与源码文件所在的目录同名。如果不同名,那么在构建、安装的过程中会以代码包名称为准。

每个代码包都会有导入路径,使用前必须先导入,例如:

import "github.com/labstack/echo"

在工作区中,一个代码包的导入路径实际上就是从 src 子目录,到该包的实际存储位置的相对路径。

源码文件通常会被放在某个工作区的 src 子目录下。在安装后如果产生了归档文件(以“.a”为扩展名),就会放进该工作区的 pkg 子目录;如果产生了可执行文件,就可能会放进该工作区的 bin 子目录。

安装某个代码包产生的归档文件与该代码包同名,放置它的相对目录就是该代码包的导入路径的直接父级。例如某个包的导入路径为github.com/labstack/echo,那么执行命令go install github.com/labstack/echo生成的归档文件的相对路径就是 github.com/labstack/echo.a

归档文件的相对目录与 pkg 目录之间还有一级平台相关的目录,由 build 的目标操作系统、下划线和目标计算架构的代号组成的,例如linux_amd64。

大致结构如下:

image-20211208213139403

构建使用命令go build,安装使用命令go install。构建和安装代码包时都会执行编译、打包等操作,并且生成的任何文件都会先被保存到某个临时的目录中。

如果构建库源码文件,结果文件只会存在于临时目录(所在工作区的 pkg 目录下的某个子目录)中,意义在于检查和验证。如果构建的是命令源码文件,结果文件会被搬运到源码文件所在的目录(所在工作区的 bin 目录)中。

库源码文件

命令源码文件是程序的运行入口,是每个可独立运行的程序必须拥有的。我们可以通过构建或安装,生成与其对应的可执行文件,后者一般会与该命令源码文件的直接父目录同名。

库源码文件是不能被直接运行的源码文件,它仅用于存放程序实体,这些程序实体可以被其他代码使用(只要遵从 Go 语言规范的话)。

在 Go 语言中,程序实体被统称为标识符,是变量、常量、函数、结构体和接口的统称。

这节我们讨论的问题是:怎样把命令源码文件中的代码拆分到其他库源码文件?

在pack目录下有一个pack1.go文件:

package main

import "flag"

var name string

func init() {
	flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
	flag.Parse()
	hello(name)
}

函数hello被声明在另一个文件中,在同一目录下的pack1_lib.go,代码如下:

package main

import "fmt"

func hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}

由于两个文件在同一目录下,我们应该将其都声明为属于同一个代码包,即package main

如果该目录下有一个命令源码文件,那么为了让同在一个目录下的文件都通过编译,其他源码文件应该也声明属于main包。

接下来我们就可以运行它们了:

>go run pack1.go pack1_lib.go
Hello, everyone!

或者先构建当前代码包之后再运行:

go build pack

报错:package pack is not in GOROOT (D:\deploy\go\src\pack)

使用go mod管理起来:

E:\go_project\pack>go mod init pack
go: creating new go.mod: module pack
go: to add module requirements and sums:
        go mod tidy

E:\go_project\pack>go build pack

E:\go_project\pack>pack.exe
Hello, everyone!

分包示例:

下面在pack目录下创建lib目录,创建pack2_lib.go文件,代码如下:

package lib

import "fmt"

func Hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}

pack目录下的pack2.go文件内容如下:

package main

import (
	"flag"
	"pack/lib"
)

var name string

func init() {
	flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
	flag.Parse()
	lib.Hello(name)
}

运行结果:

>go run pack2.go
Hello, everyone!

在Go语言中,名称的首字母为大写的程序实体才可以被当前包外的代码引用,否则它就只能被当前包内的其他代码引用。通过大小写,Go 语言自然地把程序实体的访问权限划分为包级私有和公开的。

Go 程序实体的第三种访问权限:模块级私有。具体规则是,internal代码包中声明的公开程序实体仅能被该代码包的直接父包及其子包中的代码引用。当然,引用前需要先导入这个internal包。对于其他代码包,导入该internal包都是非法的,无法通过编译。

GOPATH 构建模式

例如下面代码引入了Go 社区使用最为广泛的第三方 log 包:

package main
import "github.com/sirupsen/logrus"
func main() {
    logrus.Println("hello, gopath mode")
}

直接构建时由于Go 编译器在 GOPATH 环境变量所配置的目录下无法找到程序依赖的 logrus 包将报出如下错误:

>go build test.go
test.go:2:8: no required module provides package github.com/sirupsen/logrus; to add it:
        go get github.com/sirupsen/logrus

通过以下命令可以查看GOPATH的位置:

>go env GOPATH
C:\Users\ASUS\go

上述路径是未设置的默认路径,即用户目录下的go路径下,可以通过以下命令修改GOPATH的位置:

>go env -w GOPATH=E:\go_lib

注意:如果没有显式设置 GOPATH 环境变量,Go 会将 GOPATH 设置为默认值。

根据上面的提示可以知道,只需执行go get命令即可:

>go get github.com/sirupsen/logrus
go: downloading github.com/sirupsen/logrus v1.8.1
go: downloading golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
go get: added github.com/sirupsen/logrus v1.8.1

此时go get 命令会将 logrus 包和它依赖的包一起下载到 GOPATH 环境变量配置的目录下,同时还会将该依赖包的下载位置记录下来,后面即使将 GOPATH 目录下已经缓存的依赖包删除后,执行build构建也不会再报错,而是直接下载。

不过,go get 下载的包只是那个时刻各个依赖包的最新主线版本,Go 编译器并没有关注 Go 项目所依赖的第三方包的版本。Go 开发者希望自己的 Go 项目所依赖的第三方包版本能受到自己的控制,而不是随意变化。于是 Go 核心开发团队引入了 Vendor 机制试图解决上面的问题。

vendor 机制

Go 在 1.5 版本中引入 vendor 机制,即在 Go 项目的vendor目录下,将所有依赖包缓存起来。

Go 编译器会优先使用 vendor 目录下缓存的第三方包版本,这样,无论 GOPATH 路径下的第三方包是否存在、版本是什么,都不会影响到 Go 程序的构建。

最好将 vendor 一并提交到代码仓库中,这样其他开发者下载你的项目后,就直接可以实现可重现的构建。之前的版本中需要将Go 项目放到GOPATH的某个路径的src目录下,才可开启 vendor 机制。

上面的代码示例手动添加 vendor 目录后的代码结构:

.
├── test.go
└── vendor/
    ├── github.com/
    │   └── sirupsen/
    │       └── logrus/
    └── golang.org/
        └── x/
            └── sys/
                └── unix/

添加完 vendor 后重新编译 test.go,这个时候 Go 编译器就会在 vendor 目录下搜索程序依赖的 logrus 包以及后者依赖的 golang.org/x/sys/unix 包了。

vendor 机制下需要手工管理 vendor 下面的 Go 依赖包,而且占用代码仓库空间。为了解决这些问题,Go 核心团队推出了 Go 官方的解决方案:Go Module

Go Module 构建模式

从 Go 1.11 版本开始,Go 增加了Go Module 构建模式。

创建一个 Go Module,通常有如下几个步骤:

  1. 通过 go mod init 创建 go.mod 文件,将当前项目变为一个 Go Module;
  2. 通过 go mod tidy 命令自动更新当前 module 的依赖信息;
  3. 执行 go build,执行新 module 的构建。

首先创建go.mod 文件:

>go mod init test
go: creating new go.mod: module test
go: to add module requirements and sums:
        go mod tidy

当前创建的go.mod 文件的内容:

module test

go 1.17

按照go mod命令的提示执行go mod tidy:

>go mod tidy
go: finding module for package github.com/sirupsen/logrus
go: found github.com/sirupsen/logrus in github.com/sirupsen/logrus v1.8.1
go: downloading github.com/stretchr/testify v1.2.2
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading github.com/davecgh/go-spew v1.1.1

由 go mod tidy 下载的依赖 module 会被放置在module 的缓存路径下,默认值是 GOPATH的第一个路径下的pkg/mod 目录下,Go 1.15以上版本可以通过 GOMODCACHE 环境变量,自定义本地 module 的缓存路径。

E:\go_lib\pkg\mod\github.com (169.79KB)
└── sirupsen (169.79KB)
    └── logrus@v1.8.1 (169.79KB)

此时 go.mod 的内容更新为:

module test

go 1.17

require github.com/sirupsen/logrus v1.8.1

require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect

可以看到当前项目依赖的库和对应版本都被记录下来。

go mod 命令维护的另一个文件 go.sum,存放了特定版本 module 内容的哈希值,内容如下:

github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

目的是确保项目所依赖的 module 内容,不会被恶意或意外篡改。

最终,go build 命令会读取 go.mod 中的依赖及版本信息,并在本地 module 缓存路径下找到对应版本的依赖 module,执行编译和链接。


Go Module 构建模式设计了语义导入版本 (Semantic Import Versioning)最小版本选择 (Minimal Version Selection) 等机制。

Go Module 的语义导入版本机制

go.mod 的 require 段中依赖的版本号,都符合 vX.Y.Z 的格式。由前缀 v 和一个语义版本号组成。

语义版本号分成 3 部分:主版本号 (major)、次版本号 (minor) 和补丁版本号 (patch)。

按照语义版本规范,主版本号不同的两个版本是相互不兼容的。在主版本号相同的情况下,次版本号大都是向后兼容次版本号小的版本。补丁版本号不影响兼容性。

如果同一个包的新旧版本是兼容的,那么它们的包导入路径应该是相同的

如果一个项目依赖 logrus,无论它使用的是 v1.7.0 版本还是 v1.8.1 版本,它都可以使用下面的包导入语句导入 logrus 包:

import "github.com/sirupsen/logrus"

但如果一个项目依赖 logrus v2.0.0 版本,就与v1.x的版本不再兼容,再需要额外导入 logrus v2.0.0 版本依赖包,可以将包主版本号引入到包导入路径中:

import "github.com/sirupsen/logrus/v2"

我们可以同时依赖一个包的两个不兼容版本:

import (
    "github.com/sirupsen/logrus"
    logv2 "github.com/sirupsen/logrus/v2"
)

语义版本规范认为v0.y.z的版本号是用于项目初始开发阶段的版本号,API不稳定。于是Go Module 将 v0版本 与 主版本号 v1 做同等对待。

当依赖的主版本号为 0 或 1 的时候,在Go源码中导入依赖包不需要在包的导入路径上增加版本号:

import github.com/user/repo/v0 等价于 import github.com/user/repo
import github.com/user/repo/v1 等价于 import github.com/user/repo

但是在导入主版本号大于 1 的依赖时就必须加上版本号信息,比如导入7.x版本的Redis:

import "github.com/go-redis/redis/v7"

Go Module 的最小版本选择原则

如果项目中的两个依赖包之间存在共同依赖时,Go Module将会以最小版本选择原则选择相应版本的依赖包。

比如,myproject 有两个直接依赖 A 和 B,A 和 B 有一个共同的依赖包 C,但 A 依赖 C 的 v1.1.0 版本,而 B 依赖的是 C 的 v1.3.0 版本,并且此时 C 包的最新发布版为 C v1.7.0。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6U0mtQUD-1644673301966)(零基础决战Go语言从入门到入土.assets/image-20220119101310701.png)]

此时,Go 命令如何为 myproject 选出间接依赖包 C 的版本呢?选出的究竟是 v1.7.0、v1.1.0 还是 v1.3.0 呢?

当前存在的主流编程语言,相对GO语言来说可以称为最新最大版本原则,大概率会选择 v1.7.0版本。而Go Module 的最小版本选择原则是指选出符合项目整体要求的“最小版本”。

上述例子中,C v1.3.0 是符合项目整体要求的版本集合中的版本最小的那个,于是 Go 命令选择了 C v1.3.0。

Go 各版本构建模式机制和切换

在 Go 1.11 版本中,GOPATH 构建模式与 Go Modules 构建模式各自独立工作,我们可以通过设置环境变量 GO111MODULE 的值在两种构建模式间切换。

GO的各个版本在GO111MODULE 为不同值时的行为有所不同,下面我们以表格形式描述一下:

image-20220119104022018

Go Module的各类操作

为当前 module 添加一个依赖

比如要为一个项目增加一个新依赖:github.com/google/uuid。首先需要更新源码:

package main
import (
  "github.com/google/uuid"
  "github.com/sirupsen/logrus"
)
func main() {
  logrus.Println("hello, go module mode")
  logrus.Println(uuid.NewString())
}

我们可以执行go get命令:

>go get github.com/google/uuid
go: downloading github.com/google/uuid v1.3.0
go get: added github.com/google/uuid v1.3.0

go mod tidy命令:

>go mod tidy
go: downloading github.com/google/uuid v1.3.0

对于这个简单的例子而言,go get 新增依赖项和执行 go mod tidy 自动分析和下载依赖项的最终效果,是等价的。此时go.mod文件的内容都修改为:

module test

go 1.17

require (
	github.com/google/uuid v1.3.0
	github.com/sirupsen/logrus v1.8.1
)

require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect

但需要添加多个依赖项时,逐一手工添加依赖项显然不如直接使用go mod tidy自动分析高效。

移除一个依赖

通过 go list 命令列出当前 module 的所有依赖:

>go list -m all
test
... ...
github.com/google/uuid v1.3.0
... ...

要想彻底从项目中移除 go.mod 中的依赖项,在源码中删除对依赖项的导入语句后,还需执行 go mod tidy 命令,它会自动分析源码依赖,而且将不再使用的依赖从 go.mod 和 go.sum 中移除。

**更新依赖的版本:**默认情况下go mod tidy 命令,帮我们选择了 logrus 的当前最新发布版本 v1.8.1。如果我们想将 logrus 版本降至 v1.7.0。第一种解决方案是执行带有版本号的go get 命令:

>go get github.com/sirupsen/logrus@v1.7.0
go: downloading github.com/sirupsen/logrus v1.7.0
go get: downgraded github.com/sirupsen/logrus v1.8.1 => v1.7.0

或者我们可以直接修改go.mod文件中,依赖库对应的版本,除了手工修改文件外还支持命令修改:

go mod edit -require=github.com/sirupsen/logrus@v1.7.0

然后再执行go mod tidy 命令即可。

可以使用go list -m -versions命令查看依赖库当前发布的版本号:

>go list -m -versions github.com/sirupsen/logrus
github.com/sirupsen/logrus v0.1.0 v0.1.1 v0.2.0 v0.3.0 v0.4.0 v0.4.1 v0.5.0 v0.5.1 v0.6.0 v0.6.1 v0.6.2 v0.6.3 v0.6.4 v0.6.5 v0.6.6 v0.7.0 v0.7.1 v0.7.2 v0.7.3 v0.8.0 v0.8.1 v0.8.2 v0.8.3 v0.8.4 v0.8.5 v0.8.6 v0.8.7 v0.9.0 v0.10.0 v0.11.0 v0.11.1 v0.11.2 v0.11.3 v0.11.4 v0.11.5 v1.0.0 v1.0.1 v1.0.3 v1.0.4 v1.0.5 v1.0.6 v1.1.0 v1.1.1 v1.2.0 v1.3.0 v1.4.0 v1.4.1 v1.4.2 v1.5.0 v1.6.0 v1.7.0 v1.7.1 v1.8.0 v1.8.1

特殊情况:使用 vendor

Go Module 构建模式下,再也无需手动维护 vendor 目录下的依赖包了,Go 提供了可以快速建立和更新 vendor 的命令:

>go mod vendor
>tree -p vendor -m 2
vendor (6.69MB)
├── github.com (114.31KB)
│   ├── google (31.39KB)
│   └── sirupsen (82.92KB)
├── golang.org (6.57MB)
│   └── x (6.57MB)
└── modules.txt (417b)

go mod vendor 命令在 vendor 目录下,创建了一份这个项目的依赖包的副本,并且通过 vendor/modules.txt 记录了 vendor 下的 module 以及版本。

在 Go 1.14 及以后版本中,如果 Go 项目的顶层目录下存在 vendor 目录,那么 go build 默认也会优先基于 vendor 构建,除非显示给 go build 传入 -mod=mod 参数。

基本语法

注释与字符串格式化

在go语言中每一行代码不需要加;分号,如果需要将多个语句写在同一行则必须使用;人为区分。

go语言的注释规则如下:

// 单行注释
/*
 我是多行注释
 我是多行注释
 */

⚠ go语言的变量名由字母数字和下划线组成,第一个字符必须是下划线或字母。

字符串格式化:

package main
import "fmt"

func main() {
   // %d 表示整型数字,%s 表示字符串
    var name="百度"
    var url="www.baidu.com"
    var site=fmt.Sprintf("网站名:%s, 地址:%s",name,url)
    fmt.Println(site)
}

运行结果:网站名:百度, 地址:www.baidu.com

Go 字符串格式化符号:

格 式描 述
%v按值的本来值输出
%+v在 %v 基础上,对结构体字段名和值进行展开
%#v输出 Go 语言语法格式的值
%T输出 Go 语言语法格式的类型和值
%p指针,十六进制方式显示

这四个格式是针对结构体的,例如:

type point struct {
    x, y int
}

func main() {
    p := point{1, 2}
    fmt.Printf("%v\n", p)
    fmt.Printf("%+v\n", p)
    fmt.Printf("%#v\n", p)
    fmt.Printf("%T\n", p)
    fmt.Printf("%p\n", &p)
}   

结果:

{1 2}
{x:1 y:2}
main.point{x:1, y:2}
main.point
0xc0000120a0
格 式描 述
%%输出 % 本体
%t逻辑值
%b整型以二进制方式显示
%o整型以八进制方式显示
%d整型以十进制方式显示
%x整型以十六进制方式显示
%X整型以十六进制、字母大写方式显示
%cAscii码字符
%f浮点数
%e小写e的科学计算法
%E大写E的科学计算法

例如:

package main

import "fmt"

func main() {
	fmt.Printf("%%t=%t\n", true)
	fmt.Printf("%%b=%b\n", 14)
	fmt.Printf("%%o=%o\n", 14)
	fmt.Printf("%%d=%d\n", 14)
	fmt.Printf("%%x=%x\n", 14)
	fmt.Printf("%%X=%X\n", 14)
	fmt.Printf("%%c=%c\n", 65)
	fmt.Printf("%%f=%f\n", 78.9)
	fmt.Printf("%%e=%e\n", 123400000.0)
	fmt.Printf("%%E=%E\n", 123400000.0)
}

结果:

%t=true
%b=1110
%o=16
%d=14
%x=e
%X=E
%c=A
%f=78.900000
%e=1.234000e+08
%E=1.234000E+08

字符串对齐的示例:

package main

import "fmt"

func main() {
    fmt.Printf("|%6d|%6d|\n", 12, 345)
    fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
    fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
    fmt.Printf("|%6s|%6s|\n", "foo", "b")
    fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
}
|    12|   345|
|  1.20|  3.45|
|1.20  |3.45  |
|   foo|     b|
|foo   |b     |

格式化函数的用法:

  • 函数 fmt.SprintfPrintf 的差别在于前者将格式化后的字符串以返回值的形式返回给调用者,后者直接打印到控制台。
  • 函数 fmt.Printfmt.Println 会自动使用格式化标识符 %v 对字符串进行格式化,两者都会在每个参数之间自动增加空格,而后者还会在字符串的最后加上一个换行符。

Go 语言运算符

**算术运算符:**除了四则运算+-*/和求余%,还支持自增++和自减–

**关系运算符:**与其他语言一致==、!=、>、<、>=、<=

**逻辑运算符:**与或非&&、||、!

位运算符:&、|、^、<<、>>分别表示按位与、按位或、按位异或(取反)、左移和右移

**赋值运算符:**与其他语言一致=、+=、-=、*=、/=、%=、<<=、>>=、&=、^=、|=

指针操作:&a获取变量的实际地址,*a表示取出指针变量对应的数据

运算符优先级:* / % << >> & &^ > + - | ^ > == != < <= > >= > && > ||

Go 语言条件语句

注意:Go 不支持三目运算符

if 语句 的完整语法如下:

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
} else if 布尔表达式2 {
  /* 在布尔表达式1为 false ,布尔表达式2为 true时执行 */
} else {
  /* 在布尔表达式1和2都为 false 时执行 */
}

示例:

package main

import "fmt"

func main() {
    num := 99
    if num >= 0 && num <= 50 {
        fmt.Println("小于等于50")
    } else if num >= 51 && num <= 100 {
        fmt.Println("在51到100之间")
    } else {
        fmt.Println("大于100")
    }
}

if 还可以包含一个初始化语句,上述代码if之前的初始化可以直接包含在if中:

package main

import "fmt"

func main() {
	if num := 99; num >= 0 && num <= 50 {
		fmt.Println("小于等于50")
	} else if num >= 51 && num <= 100 {
		fmt.Println("在51到100之间")
	} else {
		fmt.Println("大于100")
	}
}

switch 语句 的标准语法如下:

switch initStmt; expr {
    case expr1:
        // 执行分支1
    case expr2:
        // 执行分支2
    case expr3_1, expr3_2, expr3_3:
        // 执行分支3
    case expr4:
        // 执行分支4
    ... ...
    case exprN:
        // 执行分支N
    default: 
        // 执行默认分支
}

initStmt 是一个可选的组成部分,exprN最终结果必须为相同类型的表达式。示例:

package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90:
		fmt.Println("优秀!")
		grade = "A"
      case 80:
		fmt.Println("良好!")
		grade = "B"
      case 50,60,70 :
		fmt.Println("不及格!")
		grade = "C"
      default:
		fmt.Println("差!")
		grade = "D"  
   }
   fmt.Printf("你的等级是 %s", grade )
}

结果:

优秀!
你的等级是 A

执行以下代码可以知道switch 语句的执行次序:

func case1() int {
    println("eval case1 expr")
    return 1
}
func case2_1() int {
    println("eval case2_1 expr")
    return 0 
}
func case2_2() int {
    println("eval case2_2 expr")
    return 2 
}
func case3() int {
    println("eval case3 expr")
    return 3
}
func switchexpr() int {
    println("eval switch expr")
    return 2
}
func main() {
    switch switchexpr() {
    case case1():
        println("exec case1")
    case case2_1(), case2_2():
        println("exec case2")
    case case3():
        println("exec case3")
    default:
        println("exec default")
    }
}

执行结果:

eval switch expr
eval case1 expr
eval case2_1 expr
eval case2_2 expr
exec case2

当 switch 表达式的类型为布尔类型时,如果求值结果始终为 true,可以省略 switch 后面的表达式,比如:

// 带有initStmt语句的switch语句
switch initStmt; {
    case bool_expr1:
    case bool_expr2:
    ... ...
}
// 没有initStmt语句的switch语句
switch {
    case bool_expr1:
    case bool_expr2:
    ... ...
}

注意:在带有 initStmt 的情况下,如果我们省略 switch 表达式,那么 initStmt 后面的分号不能省略,因为 initStmt 是一个语句。

Go 语言中的 Swith 语句取消了默认执行下一个 case 代码逻辑的“非常规”语义,每个 case 对应的分支代码执行完后就结束 switch 语句。如果需要执行下一个 case 的代码逻辑,可以显式使用 fallthrough 来实现。

当被执行的代码块中存在fallthrough 时,会直接执行下一个代码块不判断表达式的结果。示例:

func case1() int {
    println("eval case1 expr")
    return 1
}
func case2() int {
    println("eval case2 expr")
    return 2
}
func switchexpr() int {
    println("eval switch expr")
    return 1
}
func main() {
    switch switchexpr() {
    case case1():
        println("exec case1")
        fallthrough
    case case2():
        println("exec case2")
        fallthrough
    default:
        println("exec default")
    }
}

结果:

eval switch expr
eval case1 expr
exec case1
exec case2
exec default

由于 fallthrough 的存在,Go 不会对 case2 的表达式做求值操作,而会直接执行 case2 对应的代码分支。

如果某个 case 语句已经是 switch 语句中的最后一个 case 了,并且它的后面也没有 default 分支了,那么这个 case 中就不能再使用 fallthrough,否则编译器就会报错。

Go 语言循环语句

For 循环的完整形式如下:

for init; condition; post { }

可简写为for condition { }相当于其他语言的where condition

可进一步简写为for { } 相当于python语言的where True

示例:

package main

import "fmt"

func main() {
        sum,i := 0,1
        for ; i <= 5; i++ {
                sum += i
        }
        fmt.Println(sum)
}

for循环中可以同时使用多个计数器:

for i, j := 0, N; i < j; i, j = i+1, j-1 {}

For-each range 循环:

range 循环在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对,只写一个参数时则只取索引:

示例:

package main
import "fmt"

func main() {
	// 迭代数组
	strings := []string{"google", "baidu"}
	for i, s := range strings {
		fmt.Println("arr1:", i, s)
	}
	for i := range strings {
		fmt.Println("arr2:", i, strings[i])
	}
	// 迭代map
	kvs := map[string]string{"a": "apple", "b": "banana"}
	for k, v := range kvs {
		fmt.Printf("map1: %s -> %s\n", k, v)
	}
	for k := range kvs {
		fmt.Printf("map2: %s -> %s\n", k, kvs[k])
	}
	// 字符串也属于可迭代元素,迭代出来的是字符对应的Unicode编码
	for i, c := range "Go" {
        fmt.Println(i, c)
    }
}

结果:

arr1: 0 google
arr1: 1 baidu
arr2: 0 google
arr2: 1 baidu
map1: a -> apple
map1: b -> banana
map2: a -> apple
map2: b -> banana
0 71
1 111

go语言的循环除了breakcontinue外还支持goto语句,但为了避免程序混乱,一般项目都会禁止使用goto关键字。

下面我们看看如何通过break跳出多层循环:

package main

import "fmt"

func main() {
   tag:
   for i:=0;i < 10;i++ {
   		for j:=0;j < 10;j++ {
			if j>2 {
                continue tag
            }
   			fmt.Printf("i=%d,j=%d;",i,j)
   			if i==2 && j==2 {
   				break tag
   			}
   		}
   }
}

结果:

i=0,j=0;i=0,j=1;i=0,j=2;i=1,j=0;i=1,j=1;i=1,j=2;i=2,j=0;i=2,j=1;i=2,j=2;

跳不出循环的 break

Go 语言规范中明确规定,不带 label 的 break 语句跳出的是同一函数内 break 语句所在的最内层的 for、switch 或 select。示例:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1
    // find first even number of the interger slice
    for i := 0; i < len(sl); i++ {
        switch sl[i] % 2 {
        case 0:
            firstEven = sl[i]
            break
        case 1:
            // do nothing
        }        
    }         
    println(firstEven) 
}

执行结果为 12,这是因为break只跳出了当前的switch,未跳出for循环。

要跳出for循环则必须使用带 label 的 break 语句:

func main() {
    var sl = []int{5, 19, 6, 3, 8, 12}
    var firstEven int = -1
    // find first even number of the interger slice
loop:
    for i := 0; i < len(sl); i++ {
        switch sl[i] % 2 {
        case 0:
            firstEven = sl[i]
            break loop
        case 1:
            // do nothing
        }
    }
    println(firstEven) // 6
}

for-range和switch语句中的细节

for-range示例:

package main

import "fmt"

func main() {
	// 数组
	nums1 := [...]int{1, 2, 3, 4, 5, 6}
	for i, e := range nums1 {
		if i == len(nums1)-1 {
			nums1[0] += e
		} else {
			nums1[i+1] += e
		}
	}
	fmt.Println(nums1)

	// 切片
	nums2 := []int{1, 2, 3, 4, 5, 6}
	for i, e := range nums2 {
		if i == len(nums1)-1 {
			nums2[0] += e
		} else {
			nums2[i+1] += e
		}
	}
	fmt.Println(nums2)
}

nums1和nums的区别在于一个是数组一个是切片,其他处理逻辑都一致,最终结果:

[7 3 5 7 9 11]
[22 3 6 10 15 21]

可以看到两者的结果不同。这是因为切片是引用类型,数组是值类型。对于值类型的被迭代对象,range表达式作用其副本而不是原值。而对于引用,复制地址后依然可以访问到原始对象。

对于switch表达式,下面代码将报错:

	value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
	switch 1 + 3 { // 这条语句无法编译通过。
	case value1[0], value1[1]:
		fmt.Println("0 or 1")
	case value1[2], value1[3]:
		fmt.Println("2 or 3")
	case value1[4], value1[5], value1[6]:
		fmt.Println("4 or 5 or 6")
	}

上述代码中的switch表达式的结果类型是int,而那些case表达式中子表达式的结果类型却是int8,它们的类型并不相同,所以这条switch语句编译报错。

对于下面这段代码就没有问题:

	value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
	switch value2[4] {
	case 0, 1:
		fmt.Println("0 or 1")
	case 2, 3:
		fmt.Println("2 or 3")
	case 4, 5, 6:
		fmt.Println("4 or 5 or 6")
	}

如果case表达式中子表达式的结果值是无类型的常量,那么它的类型会被自动地转换为switch表达式的结果类型,又由于上述那几个整数都可以被转换为int8类型的值,所以编译通过。当然,如果自动转换失败,编译照样报错。

另外switch语句在case子句的选择上是具有唯一性的,例如下面的代码会报错:

value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value3[4] {
case 0, 1, 2:
	fmt.Println("0 or 1 or 2")
case 2, 3, 4:
	fmt.Println("2 or 3 or 4")
case 4, 5, 6:
	fmt.Println("4 or 5 or 6")
}

由于在这三个case表达式中存在结果值相等的子表达式,所以这个switch语句无法通过编译。不过这个约束只针对结果值为常量的子表达式,例如以下代码可以通过编译了:

value5 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value5[4] {
case value5[0], value5[1], value5[2]:
	fmt.Println("0 or 1 or 2")
case value5[2], value5[3], value5[4]:
	fmt.Println("2 or 3 or 4")
case value5[4], value5[5], value5[6]:
	fmt.Println("4 or 5 or26")
}

不过,这种绕过方式对用于类型判断的switch语句(以下简称为类型switch语句)就无效了。因为类型switch语句中的case表达式的子表达式,都必须直接由类型字面量表示,而无法通过间接的方式表示。

value6 := interface{}(byte(127))
switch t := value6.(type) {
case uint8, uint16:
	fmt.Println("uint8 or uint16")
case byte:
	fmt.Printf("byte")
default:
	fmt.Printf("unsupported type: %T", t)
}

上述代码编译器报错的原因是byte类型是uint8类型的别名类型,子表达式byteuint8重复了。for-range和switch语句中的细节

for-range示例:

package main

import "fmt"

func main() {
	// 数组
	nums1 := [...]int{1, 2, 3, 4, 5, 6}
	for i, e := range nums1 {
		if i == len(nums1)-1 {
			nums1[0] += e
		} else {
			nums1[i+1] += e
		}
	}
	fmt.Println(nums1)

	// 切片
	nums2 := []int{1, 2, 3, 4, 5, 6}
	for i, e := range nums2 {
		if i == len(nums1)-1 {
			nums2[0] += e
		} else {
			nums2[i+1] += e
		}
	}
	fmt.Println(nums2)
}

nums1和nums的区别在于一个是数组一个是切片,其他处理逻辑都一致,最终结果:

[7 3 5 7 9 11]
[22 3 6 10 15 21]

可以看到两者的结果不同。这是因为切片是引用类型,数组是值类型。对于值类型的被迭代对象,range表达式作用其副本而不是原值。而对于引用,复制地址后依然可以访问到原始对象。

对于switch表达式,下面代码将报错:

	value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
	switch 1 + 3 { // 这条语句无法编译通过。
	case value1[0], value1[1]:
		fmt.Println("0 or 1")
	case value1[2], value1[3]:
		fmt.Println("2 or 3")
	case value1[4], value1[5], value1[6]:
		fmt.Println("4 or 5 or 6")
	}

上述代码中的switch表达式的结果类型是int,而那些case表达式中子表达式的结果类型却是int8,它们的类型并不相同,所以这条switch语句编译报错。

对于下面这段代码就没有问题:

	value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
	switch value2[4] {
	case 0, 1:
		fmt.Println("0 or 1")
	case 2, 3:
		fmt.Println("2 or 3")
	case 4, 5, 6:
		fmt.Println("4 or 5 or 6")
	}

如果case表达式中子表达式的结果值是无类型的常量,那么它的类型会被自动地转换为switch表达式的结果类型,又由于上述那几个整数都可以被转换为int8类型的值,所以编译通过。当然,如果自动转换失败,编译照样报错。

另外switch语句在case子句的选择上是具有唯一性的,例如下面的代码会报错:

value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value3[4] {
case 0, 1, 2:
	fmt.Println("0 or 1 or 2")
case 2, 3, 4:
	fmt.Println("2 or 3 or 4")
case 4, 5, 6:
	fmt.Println("4 or 5 or 6")
}

由于在这三个case表达式中存在结果值相等的子表达式,所以这个switch语句无法通过编译。不过这个约束只针对结果值为常量的子表达式,例如以下代码可以通过编译了:

value5 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value5[4] {
case value5[0], value5[1], value5[2]:
	fmt.Println("0 or 1 or 2")
case value5[2], value5[3], value5[4]:
	fmt.Println("2 or 3 or 4")
case value5[4], value5[5], value5[6]:
	fmt.Println("4 or 5 or26")
}

不过,这种绕过方式对用于类型判断的switch语句(以下简称为类型switch语句)就无效了。因为类型switch语句中的case表达式的子表达式,都必须直接由类型字面量表示,而无法通过间接的方式表示。

value6 := interface{}(byte(127))
switch t := value6.(type) {
case uint8, uint16:
	fmt.Println("uint8 or uint16")
case byte:
	fmt.Printf("byte")
default:
	fmt.Printf("unsupported type: %T", t)
}

上述代码编译器报错的原因是byte类型是uint8类型的别名类型,子表达式byteuint8重复了。

Go 语言数据类型

Go 语言的数据类型按类别有布尔型数字类型字符串类型派生类型四种。

布尔型包含常量 true 或 false,例如:var b bool = true

Go 语言的数字类型支持整型、浮点型和复数。

整型主要有:

  • uint8 无符号 8 位整型 (0 到 255,math.MaxUint8)
  • uint16 无符号 16 位整型 (0 到 65535,math.MaxUint16)
  • uint32 无符号 32 位整型 (0 到 4294967295)
  • uint64 无符号 64 位整型 (0 到 18446744073709551615)
  • int8 有符号 8 位整型 (-128 到 127)
  • int16 有符号 16 位整型 (-32768 到 32767)
  • int32 有符号 32 位整型 (-2147483648 到 2147483647)
  • int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

可以通过增加前缀 0 来表示 8 进制数(如:077),增加前缀 0x 来表示 16 进制数(如:0xFF),以及使用 e 来表示 10 的连乘(如: 1e3 = 1000,或者 6.022e23 = 6.022 x 1e23)。

浮点型包括 float32float64

整型的零值为 0,浮点型的零值为 0.0。Go 语言中没有提供 float 类型,不像整型Go 既提供了 int16、int32 等类型,又有 int 类型。

复数包括complex64complex128

更多的数字类型:byte(字节)、rune(类似 int32) 、uintptr(用于存放一个指针)

Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。

派生类型包括:

  • 指针类型(Pointer):例如*int
  • 数组类型
  • 结构化类型(struct)
  • Channel 类型
  • 函数类型
  • 切片类型
  • 接口类型(interface)
  • Map 类型

数值字面值(Number Literal)

早期 Go 版本支持十进制、八进制、十六进制的数值字面值形式,比如:

a := 53        // 十进制
b := 0700      // 八进制,以"0"为前缀
c1 := 0xaabbcc // 十六进制,以"0x"为前缀
c2 := 0Xddeeff // 十六进制,以"0X"为前缀

Go 1.13 版本中,Go 又增加了对二进制字面值的支持和两种八进制字面值的形式,比如:

d1 := 0b10000001 // 二进制,以"0b"为前缀
d2 := 0B10000001 // 二进制,以"0B"为前缀
e1 := 0o700      // 八进制,以"0o"为前缀
e2 := 0O700      // 八进制,以"0O"为前缀

Go 1.13 版本还支持在字面值中增加数字分隔符“_”,例如:

a := 5_3_7   // 十进制: 537
b := 0b_1000_0111  // 二进制位表示为10000111 
c1 := 0_700  // 八进制: 0700
c2 := 0o_700 // 八进制: 0700
d1 := 0x_5c_6d // 十六进制:0x5c6d

注意:二进制字面值以及数字分隔符,只在 go.mod 中的 go version 指示字段为 Go 1.13 以及以后版本的时候,才会生效,否则编译器会报错。

对于浮点数,整数或小数部分如果为0,可以省略不写:

3.1415
.15  // 整数部分如果为0,整数部分可以省略不写
81.80
82. // 小数部分如果为0,小数点后的0可以省略不写

科学计数法形式表示浮点数:

6674.28e-2 // 6674.28 * 10^(-2) = 66.742800
.12345E+5  // 0.12345 * 10^5 = 12345.000000
0x2.p10  // 2.0 * 2^10 = 2048.000000
0x1.Fp+0 // 1.9375 * 2^0 = 1.937500

十六进制科学计数法的整数部分、小数部分用的都是十六进制形式,但指数部分依然是十进制形式,并且字面值中的 p 代表的幂运算的底数为 2,0x0.F转换为10进制小数为15 x 16^(-1)=0.9375

复数可以通过以下方式表示:

5 + 6i
0o123 + .12345E+5i
complex(5, 6) // 5 + 6i
complex(0o123, .12345E+5) // 83+12345i

函数 real 和 imag可获取一个复数的实部与虚部:

var c = complex(5, 6) // 5 + 6i
r := real(c) // 5.000000
i := imag(c) // 6.000000

浮点型的二进制表示

IEEE 754 标准规定了四种表示浮点数值的方式:单精度(32 位)、双精度(64 位)、扩展单精度(43 比特以上)与扩展双精度(79 比特以上,通常以 80 位实现)。Go 语言提供了 float32 与 float64 两种浮点类型,它们分别对应的就是 IEEE 754 中的单精度与双精度浮点数值类型。

IEEE 754 规范表示一个浮点数的标准形式:

符号位(S)阶码(E)尾数(M)
signexponentmaintissa

它们这样表示一个浮点数:
( − 1 ) S × 1. M × 2 E − o f f s e t \Large (-1)^{S} \times 1 . M \times 2^{E-offset} (1)S×1.M×2Eoffset
其中 offset 称为阶码偏移值。阶码部分并不直接填小数点移动而得到的指数,而是将指数加上阶码偏移值之后再进行存储,即 阶码E = 指数 + 阶码偏移值,所以 指数 = E - offset

阶码偏移值=2^(e-1)-1,其中 e 为阶码部分的 bit 位数。

单精度和双精度各部分所占位数:

所占 bit 位数符号位(S)阶码(E)尾数(M)
单精度float321823
双精度float6411152

对于 float32的单精度浮点数而言,e=8,于是单精度浮点数的阶码偏移值就为 2^(8-1)-1 = 127。

例如我们将139.8125,转换为 IEEE 754 规范的单精度二进制表示。

  1. 转换为2进制得到10001011.1101
  2. **移动小数点,直到整数部分仅有一个 1,**小数点向左移了 7 位,所以指数为7,尾数M为00010111101,不足23位的后面部分都为0
  3. 计算阶码,阶码 = 7 + 127 = 134d = 10000110b

故单精度的浮点数139.8125的二进制表示形式为0_10000110_00010111101_000000000000,即:

符号位(S)阶码(E)尾数(M)
01000011000010111101000000000000

对于 float64的单精度浮点数而言,e=11,其 阶码偏移值=2^(11-1)-1 = 1023,阶码 = 7 + 1023= 1030= 10000000110b。

故双精度的浮点数139.8125的二进制表示形式为0_10000000110_00010111101_(41个0)

通过代码验证一下:

func main() {
	var f1 float32 = 139.8125
	fmt.Printf("%b\n", math.Float32bits(f1))

	var f2 = 139.8125
	fmt.Printf("%b\n", math.Float64bits(f2))
}

结果:

1000011000010111101000000000000
100000001100001011110100000000000000000000000000000000000000000

可以看到结果等于上面我们人工计算省去最高位的 0 后得到二进制一致。

与字符相关的数字类型

Go 语言的byte 类型是 uint8 的别名,对于只占用 1 个字节的传统 ASCII 编码的字符来说,完全没有问题。下面的写法等价:

var ch byte = 'A'
var ch byte = 65
var ch byte = '\x41'

Go支持的Unicode称为 Unicode 代码点或者 runes,在内存中使用 int 来表示。在文档中,一般使用格式 U+hhhh 来表示,其中 h 表示一个 16 进制数。 rune 其实是 int32的别名。

在书写 Unicode 字符时,需要在 16 进制数之前加上前缀 \u 或者 \U

因为 Unicode 至少占用 2 个字节,所以我们使用 int16 或者 int 类型来表示。如果使用 2 字节,则加上 \u前缀;如果需要使用到 4 字节,则会加上 \U前缀。

package main

import (
	"fmt"
)

func main() {
	var ch int = '\u0041'
	var ch2 int = '\u03B2'
	var ch3 int = '\U00101234'
	fmt.Printf("%d - %d - %d\n", ch, ch2, ch3)
	fmt.Printf("%c - %c - %c\n", ch, ch2, ch3)
	fmt.Printf("%X - %X - %X\n", ch, ch2, ch3)
	fmt.Printf("%U - %U - %U", ch, ch2, ch3)
}

结果:

65 - 946 - 1053236
A - β - 􁈴
41 - 3B2 - 101234
U+0041 - U+03B2 - U+101234

%c 用于表示字符;%v%d 会输出用于表示该字符的整数;%U 输出格式为 U+hhhh 的字符串。

unicode 包含了一些针对测试字符的非常有用的函数(其中 ch 代表字符):

  • 判断是否为字母:unicode.IsLetter(ch)
  • 判断是否为数字:unicode.IsDigit(ch)
  • 判断是否为空白符号:unicode.IsSpace(ch)

这些函数返回一个布尔值。包 utf8 拥有更多与 rune 相关的函数。

package main

import (
	"fmt"
	"unicode/utf8"
)

func main() {
	str1 := "aaa你好"
	fmt.Println(len(str1), utf8.RuneCountInString(str1))
	str2 := "hello world"
	fmt.Println(len(str2), utf8.RuneCountInString(str2))
}

结果:

9 5
11 11

Go 语言的变量与常量

变量

声明变量的一般形式是使用 var 关键字:

var identifier type

可以一次声明多个变量:

var identifier1, identifier2 type

指定变量类型,如果没有初始化,则自动赋予该变量零值:int 为 0,float 为 0.0,bool 为 false,string 为空字符串,指针为 nil。

所有的内存在 Go 中都是经过初始化的。

也可以根据值自行判定变量类型。

var v_name = value
//简写为(短变量声明)
v_name := value

示例:

package main

var x, y int
var (  // 这种写法一般用于声明全局变量
    a int
    b bool
)

var c, d int = 1, 2
var e, f = 123, "hello"

func main(){
    // 不带var关键字的简写只能在函数体中出现
    g, h := 123, "hello"
    println("x, y, a, b, c, d, e, f, g, h")
    println(x, y, a, b, c, d, e, f, g, h)
}

运行结果:

>go run test3.go
x, y, a, b, c, d, e, f, g, h
0 0 0 false 1 2 123 hello 123 hello

交换变量示例:

package main

import "fmt"

func main() {
   a, b := 5, 7
   a, b = b, a
   fmt.Printf("a=%d,b=%d", a, b)
}

运行结果:

>go run t4.go
a=7,b=5

注意:在函数内部申明的变量,没有被使用在编译时会报出declared and not used的错误。已经申明的变量重复申明也会报错。

前面说到v_name := value称为短变量声明,使用它可以对同一个代码块中的变量进行重声明。示例:

package main
 
import "fmt"
 
var block = "package"
 
func main() {
    fmt.Printf("The block is %s.\n", block)
    block := "function"
    {
        block := "inner"
        fmt.Printf("The block is %s.\n", block)
    }
    fmt.Printf("The block is %s.\n", block)
}

输出:

The block is package.
The block is inner.
The block is function.

当一个函数返回多个值,部分变量并不需要时,可以使用_变量名,该变量可以接收数据却无法使用:

package main

//一个可以返回多个值的函数
func numbers()(int,int,int){
  a , b , c := 1 , 2 , 3
  return a,b,c
}

func main() {
  _,_,c := numbers() //只获取函数返回值的后两个
  println(c)
}

运行结果:

go run t5.go
3

变量作用域:

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

使用 type 关键字除了定义结构体以外还可以自定义类型,如:

type IZ int

然后我们可以使用下面的方式声明变量:

var a IZ = 5

如果有多个类型需要定义,可以使用因式分解关键字的方式,例如:

type (
   IZ int
   FZ float64
   STR string
)

go语言的类型转换语法为type_name(expression),例如:

b := int(5.0)

具有相同底层类型的变量之间可以相互转换:

var a IZ = 5
c := int(a)
d := IZ(c)

在GO语言中变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:numShipsstartDate

变量的可见性规则

当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 private )。

值类型和引用类型

与其他语言几乎一致,也分为值类型和引用类型。 int、float、bool 和 string 这些基本类型都属于值类型,值类型变量直接指向存在内存中的值,赋值时会进行值拷贝。引用类型的变量则存储了数据所在内存的地址,即第一个字所在的位置,赋值则复制地址的值。

go语言使用&符号获取变量内存地址,使用*符号获取指定内存位置的数据:

package main

func main(){
    a, b := 123, "hello"
	c, d := &a, &b
    println(c, d, *c, *d)
}

结果:

0xc00003ff50 0xc00003ff60 123 hello

Go语言常量

常量是不会被修改的量,只可以是值类型的 int、float、bool 和 string,定义格式:

const identifier [type] = value

多个相同类型的声明可以简写为:

const c_name1, c_name2 = value1, value2

示例:

package main

import "fmt"

func main() {
   const w,h int = 10,5
   var area int = w * h
   fmt.Printf("面积为 : %d", area)
}

结果:

>go run t7.go
面积为 : 50

特殊常量 iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

iota 用法示例1:

package main

func main() {
    const (
            a = iota   //0
            b          //1
            c          //2
            d = "ha"   //独立值,iota += 1
            e          //"ha"   iota += 1
            f = 100    //iota +=1
            g          //100  iota +=1
            h = iota   //7,恢复计数
            i          //8
    )
    println(a,b,c,d,e,f,g,h,i)
}

结果:

>go run t8.go
0 1 2 ha ha 100 100 7 8

iota用法示例2:

package main

const (
    i=1+iota
    j=3+iota
    k
    l
)

func main() {
    println(i,j,k,l)
}

结果:

>go run t9.go
1 4 5 6

i,j,k,l分别等价于1+0,3+1,3+2,3+3

Go 语言并没有原生提供枚举类型,在语言设计之初就将枚举类型与常量合二为一,这样就不需要再单独提供枚举类型了。示例:

const (
    _ = iota     
    Blue
    Red 
    Yellow     
) 

Go 语言函数

Go 语言最少有个 main() 函数作为程序启动入口。

函数定义格式如下:

func function_name(parameter list...) [return_types] {
   函数体
}

一个 Go 函数的声明由五部分组成,包括关键字 func函数名参数列表返回值列表函数体

示例:

package main

import "fmt"

func swap(x, y string) (string, string) {
   return y, x
}

func main() {
   a, b := swap("Google", "Baidu")
   fmt.Println(a, b)
}

结果为:

Baidu Google

函数声明中的函数名其实就是变量名,函数声明中的 func 关键字、参数列表和返回值列表共同构成了函数类型,而参数列表与返回值列表的组合也被称为函数签名

如果两个函数类型的函数签名是相同的,即便参数列表中的参数名,以及返回值列表中的返回值变量名都是不同的,那么这两个函数类型也是相同类型,比如下面两个函数类型:

func (a int, b string) (results []string, err error)
func (c int, d string) (sl []string, err error)

可以说,每个函数声明所定义的函数,仅仅是对应的函数类型的一个实例

在函数声明上的参数列表叫做形式参数(Parameter,简称形参),在函数实际调用时传入的参数被称为实际参数(Argument,简称实参)。

值传递与引用传递

值传递是指在调用函数时将实际参数逐位拷贝(Bitwise Copy)复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。指针变量声明示例如下:

  • 指向整型:var ip *int
  • 指向浮点型:var fp *float32

Go 语言中,函数参数传递采用是值传递的方式。对于像整型、数组、结构体这类类型,它们的内存表示就是它们自身的数据内容,因此当这些类型作为实参类型时,值传递拷贝的就是它们自身,传递的开销也与它们自身的大小成正比。

像 string、切片、map 这些类型它们的内存表示对应的是它们数据内容的“描述符”。当这些类型作为实参类型时,值传递拷贝的也是它们数据内容的“描述符”,不包括数据内容本身,所以这些类型传递的开销是固定的,与数据内容大小无关。这种只拷贝“描述符”,不拷贝实际数据内容的拷贝过程,也被称为**“浅拷贝”**。

下面我们先看看值传递

package main

import "fmt"

/* 定义相互交换值的函数 */
func swap(x, y int) {
   var temp int

   temp = x /* 保存 x 的值 */
   x = y    /* 将 y 值赋给 x */
   y = temp /* 将 temp 值赋给 y*/
}

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前:a=%d,b=%d\n", a ,b)
   /* 通过调用函数来交换值 */
   swap(a, b)
   fmt.Printf("交换后:a=%d,b=%d\n", a ,b)
}

执行结果为:

交换前:a=100,b=200
交换后:a=100,b=200

可以看到值传递传入的参数,由于内容拷贝,原本变量的内容并没有改变。

但我们可以通过引用传递修改传入的数据:

package main

import "fmt"

/* 定义相互交换值的函数 */
func swap(x *int, y *int) {
   var temp int
   temp = *x    /* 保存 x 地址上的值 */
   *x = *y      /* 将 y 值赋给 x */
   *y = temp    /* 将 temp 值赋给 y */
}

func main() {
   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前:a=%d,b=%d\n", a ,b)
   swap(&a, &b)
   fmt.Printf("交换后:a=%d,b=%d\n", a ,b)
}

执行结果为:

交换前:a=100, b=200
交换后:a=200, b=100

对于空指针可以使用ptr == nil判断。

可变参数

如果函数的最后一个参数是采用 ...type 的形式,那么这个函数就可以处理一个变长的参数,这个长度可以为 0,这样的函数称为变参函数。

func myFunc(a, b, arg ...int) {}

示例函数和调用:

func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")

在 Greeting 函数中,变量 who 的值为 []string{"Joe", "Anna", "Eileen"}

如果参数被存储在一个数组或切片 arr 中,则可以通过 arr... 的形式来传递参数调用变参函数。

示例:

package main

import "fmt"

func Min(a ...int) int {
	if len(a) == 0 {
		return 0
	}
	min := a[0]
	for _, v := range a {
		if v < min {
			min = v
		}
	}
	return min
}

func main() {
	x := Min(1, 3, 2, 0)
	fmt.Println(x)
	arr := []int{7, 9, 3, 5, 1}
	x = Min(arr...)
	fmt.Println(x)
}

结果:

0
1

匿名函数与函数变量

Go语言中函数可以作为变量保存起来,但只能申明在函数内部,否则会报出错误:syntax error: non-declaration statement outside function body

上述引用传递的示例,我们可以定义为一个匿名函数并通过一个变量保存起来:

package main

import "fmt"

func main() {
   /* 声明函数变量 */
	swap := func(x *int, y *int) {
	   var temp int
	   temp = *x    /* 保存 x 地址上的值 */
	   *x = *y      /* 将 y 值赋给 x */
	   *y = temp    /* 将 temp 值赋给 y */
	}

   /* 定义局部变量 */
   var a int = 100
   var b int = 200

   fmt.Printf("交换前:a=%d, b=%d\n", a ,b)
   swap(&a, &b)
   fmt.Printf("交换后:a=%d, b=%d\n", a ,b)
}

函数的返回值

函数返回值列表从形式上看主要有三种:

func foo()                       // 无返回值
func foo() error                 // 仅有一个返回值
func foo() (int, string, error)  // 有2或2个以上返回值

如果一个函数没有显式返回值,可以在函数声明中省略返回值列表。如果一个函数仅有一个返回值,那么在函数声明中就不需要将返回值用括号括起来。如果是 2 个或 2 个以上的返回值,就需要用括号括起来的。

还可以为每个返回值声明变量名,这种带有名字的返回值被称为具名返回值(Named Return Value),具名返回值变量可以像函数体中声明的局部变量一样在函数体内使用。例如fmt.Fprintf 函数的返回值:

func Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
	p := newPrinter()
	p.doPrintln(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

当函数使用 defer,而且还在 defer 函数中修改外部函数返回值时,具名返回值可以让代码显得更优雅清晰。

或者当函数的返回值个数较多时,每次显式使用 return 语句时都会接一长串返回值,这时用具名返回值可以让函数实现的可读性更好一些,比如 time 包中的 parseNanoseconds 函数:

// $GOROOT/src/time/format.go
func parseNanoseconds(value string, nbytes int) (ns int, rangeErrString string, err error) {
    if !commaOrPeriod(value[0]) {
        err = errBad
        return
    }
    if ns, err = atoi(value[1:nbytes]); err != nil {
        return
    }
    if ns < 0 || 1e9 <= ns {
        rangeErrString = "fractional second"
        return
    }
    scaleDigits := 10 - nbytes
    for i := 0; i < scaleDigits; i++ {
        ns *= 10
    }
    return
}

函数是一等公民(First-Class Citizen)

并不是在所有编程语言中函数都是“一等公民”。什么是编程语言的“一等公民”呢?wiki 发明人、C2 站点作者沃德·坎宁安 (Ward Cunningham)对“一等公民”的解释:

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为参数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。

Go 语言的函数作为“一等公民”,表现出的特征有:

  • 特征一:可以存储在变量中
  • 特征二:支持在函数内创建并通过返回值返回
  • 特征三:可作为参数传入函数
  • 特征四:拥有自己的类型

基于特征二,go语言的函数可以实现闭包,下面的getSequence函数返回了一个匿名函数,该函数在闭包中对变量i进行自增:

package main

import "fmt"

func getSequence() func() int {
   i:=0
   return func() int {
      i+=1
     return i  
   }
}

func main(){
   // 调用函数获取内部匿名函数
   nextNumber := getSequence()
   fmt.Print(nextNumber(),",")
   fmt.Print(nextNumber(),",")
   fmt.Print(nextNumber())
   fmt.Println()
   // 再来一次
   nextNumber = getSequence()
   fmt.Print(nextNumber(),",")
   fmt.Println(nextNumber())
}

结果:

1,2,3
1,2

这个匿名函数使用了定义它的函数 getSequence 的局部变量 i,这样的匿名函数在 Go 中也被称为闭包(Closure)。闭包本质上就是一个匿名函数引用了创建它们的函数中定义的变量。

闭包的另一个应用是实现偏函数,简化函数调用。例如有一个用来进行两个整型数的乘法的函数:

func times(x, y int) int {
  return x * y
}

有时我们需要反复向第二参数传入固定值,可以实现偏函数:

func partialTimes(y int) func(int) int {
  return func(x int) int {
    return times(x, y)
  }
}

这样我们就可以获取固定第二个参数的新函数:

timesFive := partialTimes(5)

基于特征三,将函数作为参数传递给函数,例如将指定文本内的所有非 ASCII 字符替换成 空格:

package main

import (
	"fmt"
	"strings"
)

func main() {
	asciiOnly := func(c rune) rune {
		if c > 127 {
			return ' '
		}
		return c
	}
	fmt.Println(strings.Map(asciiOnly, "abcd你好efg"))
}

结果:abcd efg

特征四:函数拥有自己的类型,前面已经说到每个函数声明定义的函数仅仅是对应的函数类型的一个实例,而参数列表与返回值列表组合的函数签名决定了函数的类型。

我们也可以基于函数类型来自定义类型, HandlerFunc、visitFunc 就是 Go 标准库中,基于函数类型进行自定义的类型:

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// $GOROOT/src/sort/genzfunc.go
type visitFunc func(ast.Node) ast.Visitor

所有类型都可以显式转型,而函数也拥有自己的类型,这意味着函数也可以被显式转型。最典型的示例就是 http 包中的 HandlerFunc 类型:

func greeting(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome, Gopher!\n")
}                    
func main() {
    http.ListenAndServe(":8080", http.HandlerFunc(greeting))
}

实际上 http 包的函数 ListenAndServe 接收的参数类型是Handler类型:

// $GOROOT/src/net/http/server.go
func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

而http.Handler是一个自定义的接口类型:

// $GOROOT/src/net/http/server.go
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

这意味着传入ListenAndServe的必须是一个实现了ServeHTTP方法的对象,http.HandlerFunc是如何将greeting转换为实现了Handler接口的方法的对象呢?看看其源码:

// $GOROOT/src/net/http/server.go
type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
        f(w, r)
}

可以看到,HandlerFunc 是一个基于函数类型定义的新类型,它的底层类型为函数类型func(ResponseWriter, *Request)。这个类型有一个方法 ServeHTTP,从而实现了 Handler 接口。

函数方法

Go语言可以给命名类型或结构体类型定义对应的方法,语法格式如下:

func (t *T或T) MethodName(参数列表) (返回值列表) {
    // 方法体
}

示例:

package main

import "fmt"

/* 定义结构体 */
type Circle struct {
  radius float64
}
//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
  //c.radius 即为Circle类型对象中的属性
  return 3.14 * c.radius * c.radius
}

func main() {
  var c1 Circle
  c1.radius = 10.00
  fmt.Println("圆的面积 =", c1.getArea())
}

结果:圆的面积 = 314

方法与函数的不同就在于多了一个receiver 参数, receiver 参数的类型就是这个方法归属的类型,或者说这个方法就是这个类型的一个方法。

如果在方法体中没有用到 receiver 参数,也可以省略 receiver 的参数名:

type T struct{}
func (T) M(t string) { 
    ... ...
}

Go 语言对 receiver 参数的基类型有约束,基类型本身不能为指针类型或接口类型:

type MyInt *int
func (r MyInt) String() string { // r的基类型为MyInt,编译器报错:invalid receiver type MyInt (MyInt is a pointer type)
    return fmt.Sprintf("%d", *(*int)(r))
}
type MyReader io.Reader
func (r MyReader) Read(p []byte) (int, error) { // r的基类型为MyReader,编译器报错:invalid receiver type MyReader (MyReader is an interface type)
    return r.Read(p)
}

另外Go 要求,方法声明要与 receiver 参数的基类型声明放在同一个包内

这意味着,不能为原生类型(诸如 int、float64、map 等)添加方法,不能跨越 Go 包为其他包的类型声明新方法。

defer 和追踪

关键字 defer 将延迟到函数返回之前一刻才执行某个语句或函数,类似于Java 的 finally 语句块,一般用于释放某些已分配的资源。

defer 关键字后面只能接函数(或方法),这些函数被称为 deferred 函数

示例:

package main

import "fmt"

func Function2() {
	fmt.Printf("Function2")
}

func Function1() {
	fmt.Println("top")
	defer Function2()
	fmt.Println("bottom")
}

func main() {
	Function1()
}
top
bottom
Function2

当有多个 defer 行为被注册时,它们会以逆序执行(类似栈,即后进先出):

package main

import "fmt"

func main() {
	for i := 0; i < 5; i++ {
		defer fmt.Print(i, ",")
	}
}

结果:4,3,2,1,0,

defer 将 deferred 函数注册到其所在 Goroutine 的栈数据结构中,这些 deferred 函数将按后进先出(LIFO)的顺序被程序调度执行:

image-20220211170901585

已经存储到 deferred 函数栈中的函数,最终都会被调度执行。

在 Go 1.13 前的版本中,defer 带来的开销是没有使用 defer 函数的 8 倍左右。但从 Go 1.13 版本开始,Go 核心团队对 defer 性能进行了多次优化。1.16版本defer 带来的开销并不会超过没有使用的倍。

使用 defer 语句来记录函数的参数与返回值:

package main

import (
    "io"
    "log"
)

func func1(s string) (n int, err error) {
    defer func() {
        log.Printf("func1(%q) = %d, %v", s, n, err)
    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}

错误处理

Go 函数支持多返回值机制将错误状态与返回信息分离,Go 语言惯用法是使用 error 这个接口类型表示错误,并且通常将 error 类型返回值放在返回值列表的末尾。

error 接口的定义如下:

// $GOROOT/src/builtin/builtin.go
type interface error {
    Error() string
}

errors包中的New函数和fmt.Errorf可以创建error接口类型的错误:

err := errors.New("your first demo error")
errWithCtx = fmt.Errorf("index %d is out of bounds", i)

对于go的错误处理,一般是判断调用某个可能存在异常的函数,error类型的返回值是否为nil。

示例:

package main

import (
	"errors"
	"fmt"
)

func echo(request string) (response string, err error) {
	if request == "" {
		err = errors.New("empty request")
		return
	}
	response = fmt.Sprintf("echo: %s", request)
	return
}

func main() {
	for _, req := range []string{"", "hello!"} {
		fmt.Printf("发送: %s\n", req)
		resp, err := echo(req)
		if err != nil {
			fmt.Printf("error: %s\n", err)
			continue
		}
		fmt.Printf("response: %s\n", resp)
	}
	fmt.Println()
}

结果:

发送: 
error: empty request
发送: hello!
response: echo: hello!

其实fmt.Errorf函数的本质先调用fmt.Sprintf再调用errors.New函数:

err1 := fmt.Errorf("错误内容: %s", "***")
err2 := errors.New(fmt.Sprintf("错误内容: %s", "***"))
fmt.Println(err1.Error() == err2.Error())

结果为true。

errors.New返回的 error 接口的实例的实际类型是errors包中的包级私有的类型*errorString

// $GOROOT/src/errors/errors.go
type errorString struct {
    s string
}
func (e *errorString) Error() string {
    return e.s
}

在一些场景下,错误处理者需要从错误值中提取出更多信息。比如,标准库中的 net 包就定义了一种携带额外错误上下文的错误类型:

// $GOROOT/src/net/net.go
type OpError struct {
    Op string
    Net string
    Source Addr
    Addr Addr
    Err error
}

看看标准库中的代码:

// $GOROOT/src/net/http/server.go
func isCommonNetReadError(err error) bool {
    if err == io.EOF {
        return true
    }
    if neterr, ok := err.(net.Error); ok && neterr.Timeout() {
        return true
    }
    if oe, ok := err.(*net.OpError); ok && oe.Op == "read" {
        return true
    }
    return false
}

可以看到,这段代码先判断 error 类型变量 err 的动态类型是否为 *net.OpError 或 net.Error。如果是 net.Error则通过Timeout方法判断是否超时。如果 err 的动态类型是 *net.OpError就通过它的 Op 字段判断是否为"read",从而确定是否为 CommonNetRead 类型的错误。

Go 语言的几种错误处理的惯用策略

策略一:透明错误处理策略

只要发生错误就进入唯一的错误处理执行路径,比如下面这段代码:

err := doSomething()
if err != nil {
    // 不关心err变量底层错误值所携带的具体上下文信息
    // 执行简单错误处理逻辑并返回
    ... ...
    return err
}

这是最简单的错误处理策略,完全不关心返回错误值携带的具体上下文信息。

策略二:"哨兵"错误处理策略

错误处理方需要对返回的错误值进行检视:

data, err := b.Peek(1)
if err != nil {
    switch err.Error() {
    case "bufio: negative count":
        // ... ...
        return
    case "bufio: buffer full":
        // ... ...
        return
    case "bufio: invalid use of UnreadByte":
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

这种错误策略的代码出现反模式,它以描述错误的字符串作为错误处理路径选择的依据,造成严重的隐式耦合。这意味着错误描述字符串细小的改动都会造成错误处理方处理行为的变化。

当然Go 标准库采用了定义导出的(Exported)“哨兵”错误值的方式,来辅助错误处理方检视(inspect)错误值并做出错误处理分支的决策:

// $GOROOT/src/bufio/bufio.go
var (
    ErrInvalidUnreadByte = errors.New("bufio: invalid use of UnreadByte")
    ErrInvalidUnreadRune = errors.New("bufio: invalid use of UnreadRune")
    ErrBufferFull        = errors.New("bufio: buffer full")
    ErrNegativeCount     = errors.New("bufio: negative count")
)

对错误值进行检视时可以使用这些预定义的哨兵错误:

data, err := b.Peek(1)
if err != nil {
    switch err {
    case bufio.ErrNegativeCount:
        // ... ...
        return
    case bufio.ErrBufferFull:
        // ... ...
        return
    case bufio.ErrInvalidUnreadByte:
        // ... ...
        return
    default:
        // ... ...
        return
    }
}

对于 API 的开发者而言,暴露“哨兵”错误值也意味着这些错误值和包的公共函数 / 方法一起成为了 API 的一部分。一旦发布出去,开发者就要对它进行很好的维护。而“哨兵”错误值也让使用这些值的错误处理方对它产生了依赖。

从 Go 1.13 版本开始,标准库 errors 包提供了 Is 函数用于错误处理方对错误值的检视:

// 类似 if err == ErrOutOfBounds{ … }
if errors.Is(err, ErrOutOfBounds) {
    // 越界的错误处理
}

errors.Is 方法会沿着错误所在错误链(Error Chain),与链上所有被包装的错误(Wrapped Error)进行比较,直至找到一个匹配的错误为止。例如:

var ErrSentinel = errors.New("the underlying sentinel error")

func main() {
	err1 := fmt.Errorf("wrap sentinel: %w", ErrSentinel)
	err2 := fmt.Errorf("wrap err1: %w", err1)
	println(err2 == ErrSentinel) //false
	if errors.Is(err2, ErrSentinel) {
		println("err2 is ErrSentinel")
		return
	}
	println("err2 is not ErrSentinel")
}

结果:

false
err2 is ErrSentinel

可以看到 err2 与 ErrSentinel 直接进行比较,这二者并不相同。而 errors.Is 函数则会沿着 err2 所在错误链,向下找到被包装到最底层的“哨兵”错误值ErrSentinel。

策略三:错误值类型检视策略

如果遇到错误处理方需要错误值提供更多的“错误上下文”的情况,则需要类型断言机制(Type Assertion)或类型选择机制(Type Switch)得到底层错误类型携带的错误上下文信息,这种错误处理方式可称为错误值类型检视策略

json 包中自定义了一个UnmarshalTypeError的错误类型:

// $GOROOT/src/encoding/json/decode.go
type UnmarshalTypeError struct {
    Value  string       
    Type   reflect.Type 
    Offset int64        
    Struct string       
    Field  string      
}

错误类型检视策略示例,使用类型选择机制(Type Switch)获得错误值的错误上下文信息:

// $GOROOT/src/encoding/json/decode.go
func (d *decodeState) addErrorContext(err error) error {
    if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 {
        switch err := err.(type) {
        case *UnmarshalTypeError:
            err.Struct = d.errorContext.Struct.Name()
            err.Field = strings.Join(d.errorContext.FieldStack, ".")
            return err
        }
    }
    return err
}

从 Go 1.13 版本开始,标准库 errors 包提供了As函数用于判断变量是否为特定的自定义错误类型:

// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

errors.As函数与Is 函数一样也会沿着错误所在错误链在链上查找直至找到一个匹配的错误类型:

type MyError struct {
    e string
}
func (e *MyError) Error() string {
    return e.e
}
func main() {
    var err = &MyError{"MyError error demo"}
    err1 := fmt.Errorf("wrap err: %w", err)
    err2 := fmt.Errorf("wrap err1: %w", err1)
    var e *MyError
    if errors.As(err2, &e) {
        println("MyError is on the chain of err2")
        println(e == err)
        return
    }
    println("MyError is not on the chain of err2")
}

结果:

MyError is on the chain of err2
true

errors.As函数沿着 err2 所在错误链向下查找,最终将 err2 与其类型 * MyError成功匹配,并将匹配到的错误值存储到第二个参数中。

策略四:错误行为特征检视策略

将某个包中的错误类型归类,统一提取出一些公共的错误行为特征,并将这些错误行为特征放入一个公开的接口类型中。这种方式也被叫做错误行为特征检视策略。

例如,标准库中的net包将包内的所有错误类型的公共行为特征抽象并放入net.Error这个接口中:

// $GOROOT/src/net/net.go
type Error interface {
    error
    Timeout() bool  
    Temporary() bool
}

Timeout 用来判断是否是超时(Timeout)错误,Temporary 用于判断是否是临时(Temporary)错误。错误处理方只需要依赖这个公共接口,就可以检视具体错误值的错误行为特征信息,并根据这些信息做出后续错误处理分支选择的决策。

http 包使用错误行为特征检视策略进行错误处理的示例:

// $GOROOT/src/net/http/server.go
func (srv *Server) Serve(l net.Listener) error {
    ... ...
    for {
        rw, e := l.Accept()
        if e != nil {
            select {
            case <-srv.getDoneChan():
                return ErrServerClosed
            default:
            }
            if ne, ok := e.(net.Error); ok && ne.Temporary() {
                // 注:这里对临时性(temporary)错误进行处理
                ... ...
                time.Sleep(tempDelay)
                continue
            }
            return e
        }
        ...
    }
    ... ...
}

Accept 方法实际上返回的错误类型为*OpError,它是 net 包中的一个自定义错误类型,它实现了错误公共特征接口net.Error:

// $GOROOT/src/net/net.go
type OpError struct {
    ... ...
    // Err is the error that occurred during the operation.
    Err error
}

type temporary interface {
    Temporary() bool
}

func (e *OpError) Temporary() bool {
  if ne, ok := e.Err.(*os.SyscallError); ok {
      t, ok := ne.Err.(temporary)
      return ok && t.Temporary()
  }
  t, ok := e.Err.(temporary)
  return ok && t.Temporary()
}

因此,OpError 实例可以被错误处理方通过net.Error接口的方法,判断它的行为是否满足 Temporary 或 Timeout 特征。

panic与recover

Go语言的panic不是错误,错误是可预期的,有对应的公开错误码和错误处理预案,但panic却是少见意料之外的。可以说Go语言的错误类似于Java语言的异常(Exception),而panic 类似于Java语言的运行时异常(RuntimeException)。很多教材和文章将 panic 翻译为运行时恐慌。

panic 出现后,如果没有被捕获并恢复,Go 程序的执行就会被终止,即便出现异常的位置不在主 Goroutine 中。

当发生像数组下标越界或以 0 作为分母的情况时,Go 运行时会触发 panic(运行时恐慌),伴随着程序的崩溃抛出一个runtime.Error 接口类型的值。这个错误值有个 RuntimeError() 方法用于区别普通错误。

当我们认为当前错误导致程序不能继续运行时,可以使用panic 函数产生一个中止程序的运行的错误。panic 接收一个做任意类型的参数,通常是字符串,在程序结束运行时会被打印出来。

一旦 panic 被触发,后续 Go 程序的执行过程被称为 panicking

当函数 F 调用 panic 函数时,函数F的执行被中止。所有的 defer 语句都会保证执行并把控制权交还给接收到 panic 的函数调用者。这样向上冒泡直到最顶层,并执行每层的 defer,在栈顶处程序崩溃,并在命令行中用传给 panic 的值报告错误情况。

示例:

func foo() {
	fmt.Println("call foo")
	bar()
	fmt.Println("exit foo")
}
func bar() {
	defer func() {
		if e := recover(); e != nil {
			fmt.Println("recover the panic:", e)
		}
	}()
	
	fmt.Println("call bar")
	panic("panic occurs in bar")
	zoo()
	fmt.Println("exit bar")
}
func zoo() {
	fmt.Println("call zoo")
	fmt.Println("exit zoo")
}
func main() {
	fmt.Println("call main")
	foo()
	fmt.Println("exit main")
}

执行结果:

call main
call foo
call bar
panic: panic occurs in bar

Go 也提供了 recover 函数用于捕捉 panic 并恢复程序正常执行秩序,recover 只能在 defer 修饰的函数中使用,正常执行未触发 panic 的函数调用 recover 会返回 nil。

上述例子中,bar函数触发 panic ,recover 调用示例如下:

func bar() {
	defer func() {
		if e := recover(); e != nil {
			fmt.Println("recover the panic:", e)
		}
	}()
	
	fmt.Println("call bar")
	panic("panic occurs in bar")
	zoo()
	fmt.Println("exit bar")
}

再次运行结果:

call main
call foo
call bar
recover the panic: panic occurs in bar
exit foo
exit main

从规范来说,实现者应该从 panic 中 recover,将 panic 转换成 error 告诉调用方为何出错:

package main

import (
	"fmt"
	"strconv"
)

func str2num(input string) (numbers int) {
	num, err := strconv.Atoi(input)
	if err != nil {
		panic("解析失败:" + input)
	}
	numbers = num
	return
}

// 将字符串解析为整数数组
func Parse(input string) (number int, err error) {
	defer func() {
		if r := recover(); r != nil {
			var ok bool
			err, ok = r.(error)
			if !ok {
				err = fmt.Errorf("pkg: %v", r)
			}
		}
	}()
	number = str2num(input)
	return
}
func main() {
	var examples = []string{"123", "s34", "567"}
	for _, ex := range examples {
		fmt.Printf("解析 %q,结果:\n\t", ex)
		nums, err := Parse(ex)
		if err != nil {
			fmt.Println(err)
			continue
		}
		fmt.Println(nums)
	}
}

结果:

解析 "123",结果:
	123
解析 "s34",结果:
	pkg: 解析失败:s34
解析 "567",结果:
	567

数组与切片(Slice)

数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:

var variable_name [SIZE] variable_type

例如定义一个数组 balance 长度为 10 类型为 float32:

var balance [10] float32

数组初始化:

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
// balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

使用 代替数组的长度或者直接省略,编译器会自动推断:

var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

可以通过指定下标来初始化元素:

//  将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}

这种下标定义的方式可以省略数组长度,go语言会根据最大角标自动设置长度:

balance := []float32{4:2.0,1:7.0}
fmt.Println(balance)

结果为:[0 7 0 0 2]

数组类型在实际内存分配时占据着一整块内存,如果两个数组所分配的内存大小不同,那么它们肯定是不同的数组类型。预定义函数 len 可以用于获取一个数组类型变量的长度,通过 unsafe 包提供的 Sizeof 函数,我们可以获得一个数组变量的总大小:

var arr = [6]int{1, 2, 3, 4, 5, 6}
fmt.Println("数组长度:", len(arr))           // 6
fmt.Println("数组大小:", unsafe.Sizeof(arr)) // 48

切片

**切片(slice)与数组的区别在于数组的长度是固定的,而切片是可变长的。**与数组声明相比,切片声明仅仅是少了一个“长度”属性。可以说,数组的长度是其类型的一部分。比如,[1]string[2]string就是两个不同的数组类型。

Go 语言中未指定大小的数组就是切片,定义语法如下:

var identifier []type

数组切片作为函数参数的示例:

package main

import "fmt"

func getAverage(arr []int, size int) float32 {
   sum,i := 0,0
   for ; i < size; i++ {
      sum += arr[i]
   }
   return float32(sum) / float32(size);
}

func main() {
   var balance = []int {1000, 2, 3, 17, 50}
   fmt.Printf("平均值为: %f ", getAverage(balance, 5));
}

Go 切片的结构体如下:

type slice struct {
    array unsafe.Pointer // 指向底层数组的指针
    len   int // 切片中当前元素的个数
    cap   int // 底层数组的长度,即切片的最大容量
}

Go 编译器会自动为每个新创建的切片,建立一个底层数组,默认底层数组的长度与切片初始元素个数相同。

也可以使用 make() 函数来创建切片:

var slice1 []type = make([]type, len, capacity)
// 也可以简写为
slice1 := make([]type, len)

capacity 为可选参数用于指定容量。一个切片在未初始化之前默认为 nil,长度为 0。如果不指明其容量,那么它就会和长度一致。

例如:

package main

import "fmt"

func main() {
   var x = make([]int,3,5)
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
   var y []int
   fmt.Printf("len=%d cap=%d slice=%v\n",len(y),cap(y),y)
   if(y == nil){
      fmt.Printf("切片是空的")
   }
}

结果:

len=3 cap=5 slice=[0 0 0]
len=0 cap=0 slice=[]
切片是空的

数组的切片化

采用 array[low : high : max]语法基于一个已存在的数组或切片创建切片。例如:

arr := [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
sl := arr[3:7:9]

此时的内存表现为:

image-20220208194831197

基于数组创建的切片,它的起始元素从 low 所标识的下标值开始,切片的长度(len)是 high - low,它的容量是 max - low。而且,由于切片 sl 的底层数组就是数组 arr,对切片 sl 中元素的修改将直接影响数组 arr 变量。例如:

sl[0] += 10
fmt.Println("arr[3] =", arr[3]) // 14

当然针对一个已存在的数组,还可以建立多个操作数组的切片,这些切片共享同一底层数组,切片对底层数组的操作也同样会反映到其他切片中。

也可以基于切片创建切片

package main

import "fmt"

func main() {
   numbers := []int{0,1,2,3,4,5,6,7,8}  
   fmt.Println(numbers,len(numbers),cap(numbers))
   x := numbers[1:4]
   fmt.Println("numbers[1:4] ==", x, len(x), cap(x))
   x=numbers[:3]
   fmt.Println("numbers[:3] ==", x, len(x), cap(x))
   x=numbers[4:]
   fmt.Println("numbers[4:] ==", x, len(x), cap(x))

   n1 := make([]int,0,5)
   fmt.Println(n1,len(n1),cap(n1))
   n2 := n1[:2]
   fmt.Println(n2,len(n2),cap(n2))
   n3 := n1[2:5]
   fmt.Println(n3,len(n3),cap(n3))
}

输出结果为:

[0 1 2 3 4 5 6 7 8] 9 9
numbers[1:4] == [1 2 3] 3 8
numbers[:3] == [0 1 2] 3 9
numbers[4:] == [4 5 6 7 8] 5 5
[] 0 5
[0 0] 2 5
[0 0 0] 3 3

切片对应的底层数组是无法改变的,切片代表的窗口可以向右扩展,直至其底层数组的末尾,但无法向左扩展的。把切片的窗口向右扩展到最大的方法:numbers[:cap(numbers)]

append() 和 copy() 函数

如果想增加切片的容量,必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

下面是拷贝切片的 copy 方法和向切片追加新元素的 append 方法:

package main

import "fmt"

func main() {
	var numbers []int
	printSlice(numbers)

	/* 允许追加空切片 */
	numbers = append(numbers, 0)
	printSlice(numbers)

	/* 向切片添加一个元素 */
	numbers = append(numbers, 1)
	printSlice(numbers)

	/* 同时添加多个元素 */
	numbers = append(numbers, 2, 3, 4, 5)
	printSlice(numbers)

	/* 创建切片 numbers1 是之前切片的两倍容量*/
	numbers1 := make([]int, len(numbers), (cap(numbers))*2)

	/* 拷贝 numbers 的内容到 numbers1 */
	copy(numbers1, numbers)
	printSlice(numbers1)
}

func printSlice(x []int) {
	fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x)
}

结果:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=6 cap=6 slice=[0 1 2 3 4 5]
len=6 cap=12 slice=[0 1 2 3 4 5]

append 函数常见操作:

  1. 将切片 b 的元素追加到切片 a 之后:a = append(a, b...)

  2. 复制切片 a 的元素到新的切片 b 上:

    b = make([]T, len(a))
    copy(b, a)
    
  3. 删除位于索引 i 的元素:a = append(a[:i], a[i+1:]...)

  4. 切除切片 a 中从索引 i 至 j 位置的元素:a = append(a[:i], a[j:]...)

  5. 为切片 a 扩展 j 个元素长度:a = append(a, make([]T, j)...)

  6. 在索引 i 的位置插入元素 x:a = append(a[:i], append([]T{x}, a[i:]...)...)

  7. 在索引 i 的位置插入长度为 j 的新切片:a = append(a[:i], append(make([]T, j), a[i:]...)...)

  8. 在索引 i 的位置插入切片 b 的所有元素:a = append(a[:i], append(b, a[i:]...)...)

  9. 取出位于切片 a 最末尾的元素 x:x, a = a[len(a)-1], a[:len(a)-1]

  10. 将元素 x 追加到切片 a:a = append(a, x)

一个切片的底层数组永远不会被替换。虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是同时也生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。

在无需扩容时,append函数返回的是指向原底层数组的新切片,而在需要扩容时,append函数返回的是指向新底层数组的新切片。

切片的扩容策略

估算切片容量的增长:一般情况下,当原切片的长度小于1024时,新切片的容量将会是原切片容量的 2 倍。如果一次追加的元素过多,以至于使新长度比原容量的 2 倍还要大,那么新容量就会以新长度为基准,最终的新容量一般比新容量基准更大一些。

当原切片的长度大于等于1024时,将以1.25倍作为基准,不断地与1.25相乘,直到结果不小于原长度与要追加的元素数量之和,最终,新容量往往会比新长度大一些。

更多细节可参见runtime包中 slice.go 文件里的growslice及相关函数的具体实现。

示例:

package main

import "fmt"

func main() {
	// 原切片的长度小于1024时
	s1 := make([]int, 0)
	printSlice(s1)
	for i := 1; i <= 5; i++ {
		s1 = append(s1, i)
		printSlice(s1)
	}
	fmt.Println()

    // 追加的元素过多,新长度超过其2倍时
    s2 := make([]int, 10)
    printSlice(s2)
    s2 = append(s2, make([]int, 11)...)
    printSlice(s2)
    s2 = append(s2, make([]int, 23)...)
    printSlice(s2)
    s2 = append(s2, make([]int, 45)...)
    printSlice(s2)
    fmt.Println()

	// 原切片的长度大于等于1024时
	s3 := make([]int, 1024)
	printSlice(s3)
	s3 = append(s3, make([]int, 200)...)
	printSlice(s3)
	s3 = append(s3, make([]int, 400)...)
	printSlice(s3)
	s3 = append(s3, make([]int, 600)...)
	printSlice(s3)
	fmt.Println()


}
func printSlice(x []int) {
	fmt.Printf("len=%d cap=%d\n", len(x), cap(x))
}

输出:

len=0 cap=0
len=1 cap=1
len=2 cap=2
len=3 cap=4
len=4 cap=4
len=5 cap=8

len=10 cap=10
len=21 cap=22
len=44 cap=44
len=89 cap=96

len=1024 cap=1024
len=1224 cap=1280
len=1624 cap=2048
len=2224 cap=2560

切片的底层指向一个数组,该数组的实际体积可能要大于切片所定义的体积。只有在没有任何切片指向的时候,底层的数组内层才会被释放,这种特性有时会导致程序占用多余的内存。

读取文件避免底层数组过大

函数 FindDigits 将一个文件加载到内存,然后搜索其中所有的数字并返回一个切片。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

这段代码返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存。

想要避免这个问题,可以通过拷贝我们需要的部分到一个新的切片中:

func FindDigits(filename string) []byte {
   b, _ := ioutil.ReadFile(filename)
   b = digitRegexp.Find(b)
   c := make([]byte, len(b))
   copy(c, b)
   return c
}

多维数组与多维切片

数组类型自身也可以作为数组元素的类型,这样就会产生多维数组。比如:

var mArr [2][3][4]int

从左向右逐维地去看,就可以将一个多维数组分层拆解。mArr 的两个元素分别为 mArr[0]和 mArr [1],它们的类型均为[3] [4]int。而mArr[0]的三个元素分别为 mArr[0][0]mArr[0][1]以及 mArr[0][2],它们的类型均为 [4]int,它们都是一维数组。

多维数组无论多少维,最终都由一维数组组成。在 C 语言中,数组变量可视为指向数组第一个元素的指针,而Go 传递数组的方式都是纯粹的值拷贝,这会带来较大的内存拷贝开销。

下面演示一下多维切片:

package main

import "fmt"

func main() {
    // 创建数组
    values := [][]int{}
    // 使用 appped() 函数向空的二维数组添加两行一维数组
    values = append(values, []int{1, 2, 3})
    values = append(values, []int{4, 5, 6})
	fmt.Println("数组整体:",values, ",第一行:",values[0], ",第二行:",values[1])
}

结果:

数组整体: [[1 2 3] [4 5 6]] ,第一行: [1 2 3] ,第二行: [4 5 6]

可以直接申明二维数组,示例:

sites := [][]string{{"Google","Baidu"},{"Taobao","Weibo"}}
a := [][]int{
 {0, 1, 2, 3} ,
 {4, 5, 6, 7} ,
 {8, 9, 10, 11},
}

在go语言中大括号不能单独一行,所以倒数第二行的} 必须要有逗号。或者我们可以这样写:

a := [][]int{  
 {0, 1, 2, 3} ,
 {4, 5, 6, 7} ,
 {8, 9, 10, 11}}

二维数组支持添加多个长度不一致的一维数组,可以使用append函数进行添加:

package main

import "fmt"

func main() {
    // 创建空的二维数组
    nums := [][]int{}
	nums = append(nums, []int{1})
    nums = append(nums, []int{1, 2})
    nums = append(nums, []int{1, 2, 3})
	fmt.Println("整体:", nums)
    
    // 循环输出
    for i,num := range nums {
        fmt.Printf("Row%v:%v", i, num)
        fmt.Println()
    }
}

结果:

整体: [[1] [1 2] [1 2 3]]
Row0:[1]
Row1:[1 2]
Row2:[1 2 3]

练习1:给定 slice s[]int 和一个 int 类型的因子,扩展 s 使其长度为 len(s) * factor

package main

import "fmt"

var s []int

func main() {
	s = []int{1, 2, 3}
	fmt.Println(s,len(s))
	s = enlarge(s, 5)
	fmt.Println(s,len(s))
}

func enlarge(s []int, factor int) []int {
	ns := make([]int, len(s)*factor)
	copy(ns, s)
	return ns
}

结果:

[1 2 3] 3
[1 2 3 0 0 0 0 0 0 0 0 0 0 0 0] 15

**练习2:**构造一个类似Python的map函数,将整数列表每个元素乘以10

package main

import "fmt"

func mapFunc(mf func(int) int, list []int) []int {
	result := make([]int, len(list))
	for ix, v := range list {
		result[ix] = mf(v)
	}
	return result
}

func main() {
	list := []int{0, 1, 2, 3, 4, 5, 6, 7}
	mf := func(i int) int {
		return i * 10
	}
	fmt.Println(mapFunc(mf, list))
}

结果:

[0 10 20 30 40 50 60 70]

**练习3:**构造一个类似Python的filter函数,返回满足条件的元素切片

package main

import "fmt"

func main() {
	s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
	s = Filter(s, even)
	fmt.Println(s)
}

func Filter(s []int, fn func(int) bool) []int {
	var p []int
	for _, e := range s {
		if fn(e) {
			p = append(p, e)
		}
	}
	return p
}
// 判断是否是偶数
func even(n int) bool {
	if n%2 == 0 {
		return true
	}
	return false
}

结果:

[0 2 4 6 8]

**练习4:**写一个函数 InsertStringSlice 将切片插入到另一个切片的指定位置。

package main

import "fmt"

func main() {
	s := []string{"M", "N", "O", "P", "Q", "R"}
	in := []string{"A", "B", "C"}
	res := InsertStringSlice(s, in, 0)
	fmt.Println(res)
	res = InsertStringSlice(s, in, 3)
	fmt.Println(res)
}

func InsertStringSlice(slice []string, insertion []string, index int) []string {
	result := make([]string, len(slice)+len(insertion))
	at := copy(result, slice[:index])
	at += copy(result[at:], insertion)
	copy(result[at:], slice[index:])
	return result
}

结果:

[A B C M N O P Q R]
[M N O A B C P Q R]

练习5:写一个函数 RemoveStringSlice 将从 start 到 end 索引的元素从切片 中移除。

package main

import "fmt"

func main() {
	s := []string{"A", "B", "C", "D", "E", "F"}
	res := RemoveStringSlice(s, 2, 4)
	fmt.Println(res)
}

func RemoveStringSlice(slice []string, start, end int) []string {
	result := make([]string, len(slice)+start-end)
	at := copy(result, slice[:start])
	copy(result[at:], slice[end:])
	return result
}

结果:

[A B E F]

字符串

普通字符串的申明:

const (
  GO_SLOGAN = "less is more" // GO_SLOGAN是string类型常量
  s1 = "hello, gopher"       // s1是string类型常量
)
var s2 = "I love go" // s2是string类型变量

原始字符串(Raw String)申明:

var s string = `         ,_---~~~~~----._
    _,,_,*^____      _____*g*\"*,--,
   / __/ /'     ^.  /      \ ^@q   f
  [  @f | @))    |  | @))   l  0 _/
   \/   \~____ / __ \_____/     \
    |           _l__l_           I
    }          [______]           I
    ]            | | |            |
    ]             ~ ~             |
    |                            |
     |                           |`
fmt.Println(s)

字符串是不可变的,但本质上是一个字节数组,c := []bytes(s)copy(dst []byte, src string)可以获取一个字节的切片 c。

也可以将字符串转换为int 数组或rune 数组: c := []int(s)r := []rune(s)

可以通过代码 len([]int(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点。

还可以将一个字符串追加到某一个字符数组的尾部:

var b []byte
var s string
b = append(b, s...)

字符串和切片的内存结构:

在内存中,一个字符串包含一个指向实际数据的指针和记录长度的整数。因为指针对用户完全不可见,因此依旧可以把字符串看做是一个字符数组。

字符串 string s = "hello" 和子字符串 t = s[2:3] 在内存中的结构可以用下图表示:

image-20220212212827560

Go 语言中的字符串是不可变的,需要修改字符串时,可以先将字符串转换成字节数组,然后再修改数组中的元素值,最后将字节数组转换回字符串格式。

例如,将字符串 “hello” 转换为 “cello”, “你好” 转换为 “他好”:

package main

import "fmt"

func main() {
	s := "hello你好"
	c := []rune(s)
	c[0] = 'c'
	c[5] = '他'
	s2 := string(c)
	fmt.Println(s2)
}

根据rune类型的声明type rune = int32可知,它实际上就是int32类型的一个别名类型。Go 使用 rune 这个类型来表示一个 Unicode 码点。

package main

import "fmt"

func main() {
	str := "Go 爱好者"
	fmt.Printf("runes(char): %q\n", []rune(str))
	fmt.Printf("runes(hex): %x\n", []rune(str))
	fmt.Printf("bytes(hex): [% x]\n", []byte(str))
}

输出:

runes(char): ['G' 'o' ' ' '爱' '好' '者']
runes(hex): [47 6f 20 7231 597d 8005]
bytes(hex): [47 6f 20 e7 88 b1 e5 a5 bd e8 80 85]

for-range循环迭代字符串:

	str := "Go 爱好者"
	for i, c := range str {
		fmt.Printf("%d: %q [% x]\n", i, c, []byte(string(c)))
	}

结果:

0: 'G' [47]
1: 'o' [6f]
2: ' ' [20]
3: '爱' [e7 88 b1]
6: '好' [e5 a5 bd]
9: '者' [e8 80 85]

从结果可以,遍历出了对应字节所在字符串的索引和对应 Unicode 字符。

Go 语言中的一个string类型值会由若干个 Unicode 字符组成,每个 Unicode 字符都可以由一个rune类型的值来承载。

这些字符在底层都会被转换为 UTF-8 编码值,而这些 UTF-8 编码值又会以字节序列的形式表达和存储。因此,一个string类型的值在底层就是一个能够表达若干个 UTF-8 编码值的字节序列。

go语言也支持**Unicode 专用的转义字符\u 或\U 作为前缀,**来表示一个 Unicode 字符,比如:

'\u4e2d'     // 字符:中
'\U00004e2d' // 字符:中
'\u0027'     // 单引号字符

当然还可以直接用整数值作为字符字面量给 rune 变量赋值:

'\x27'  // 使用十六进制表示的单引号字符
'\047'  // 使用八进制表示的单引号字符

字符串字面值示例:

"abc\n"
"中国人"
"\u4e2d\u56fd\u4eba" // 中国人
"\U00004e2d\U000056fd\U00004eba" // 中国人
"中\u56fd\u4eba" // 中国人,不同字符字面值形式混合在一起
"\xe4\xb8\xad\xe5\x9b\xbd\xe4\xba\xba" // 十六进制表示的字符串字面值:中国人

Go 字符串中的每个 Unicode 字符都是以 UTF-8 编码格式存储在内存当中的,所以GO语言也支持utf-8编码形式的十六进制形式表示字符串。

**练习1:**反转字符串

package main

import "fmt"

func reverse(s string) string {
	runes := []rune(s)
	n, h := len(runes), len(runes)/2
	for i := 0; i < h; i++ {
		runes[i], runes[n-1-i] = runes[n-1-i], runes[i]
	}
	return string(runes)
}

func main() {
	s := "被反转的中文字符串!"
	fmt.Println(s, " --> ", reverse(s))
}

结果:

被反转的中文字符串!  -->  !串符字文中的转反被

**练习2:**字符串去重

package main

import "fmt"

func uniqStr(s string) string {
	var arr []byte = []byte(s)
	arru := make([]byte, len(arr))
	i,tmp := 0,byte(0)
	for _, val := range arr {
		if val != tmp {
			arru[i] = val
			i++
		}
		tmp = val
	}
	return string(arru)
}

func main() {
	fmt.Println(uniqStr("abaaacdefg"))
}

结果:

abacdefg

Map

Map 是使用 hash 表来实现的无序的键值对集合。map 的内部实现要比切片复杂得多,它是由 Go 编译器与运行时联合实现的。Go 编译器在编译阶段会将语法层面的 map 操作,重写为运行时对应的函数调用。Go 运行时则采用了高效的算法实现了 map 类型的各类操作。

创建map

可以使用内建函数 make 也可以使用 map 关键字来定义 Map:

/* 声明变量,默认 map 是 nil */
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)

由于 map 要保证 key 的唯一性,对 key 的类型有严格的要求,必须支持“==”和“!=”两种比较操作符。

在 Go 语言中,函数类型、map 类型自身,以及切片只支持与 nil 的比较,而不支持同类型两个变量的比较:

s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
m1 := make(map[int]string)
m2 := make(map[int]string)
println(s1 == s2) // 错误:invalid operation: s1 == s2 (slice can only be compared to nil)
println(f1 == f2) // 错误:invalid operation: f1 == f2 (func can only be compared to nil)
println(m1 == m2) // 错误:invalid operation: m1 == m2 (map can only be compared to nil)

因此,函数类型、map 类型自身,以及切片类型是不能作为 map 的 key 类型的

可以这样声明一个 map 变量:

var m map[string]int // 一个map[string]int类型的变量

和切片类型变量一样,如果没有显式地赋予 map 变量初值,map 类型变量的默认值为 nil。

初值为零值 nil 的切片类型变量,可以借助内置的 append 的函数进行操作,这种在 Go 语言中被称为“零值可用”。但 map 类型因为它内部实现的复杂性,无法“零值可用”:

var m map[string]int // m = nil
m["key"] = 1         // 发生运行时异常:panic: assignment to entry in nil map

为 map 类型变量显式赋值有两种方式:一种是使用复合字面值;另外一种是使用 make 这个预声明的内置函数。示例:

countryCapitalMap := map[string]string{}
// 或
countryCapitalMap := make(map[string]string)

稍微复杂一些的 map 变量初始化:

m1 := map[int][]string{
    1: []string{"val1_1", "val1_2"},
    3: []string{"val3_1", "val3_2", "val3_3"},
    7: []string{"val7_1"},
}
type Position struct { 
    x float64 
    y float64
}
m2 := map[Position]string{
    {29.935523, 52.568915}: "school",
    {25.352594, 113.304361}: "shopping-mall",
    {73.224455, 111.804306}: "hospital",
}

对于m2 的显式初始化,Go 提供了“语法糖”,Go 允许省略字面值中的元素类型

使用 make 为 map 类型变量进行显式初始化:

m1 := make(map[int]string) // 未指定初始容量
m2 := make(map[int]string, 8) // 指定初始容量为8

map的插入、遍历与检查

示例:

package main

import "fmt"

func main() {
    // 创建集合
    countryCapitalMap := make(map[string]string)
    countryCapitalMap ["France"] = "巴黎"
    countryCapitalMap ["Italy"] = "罗马"
    countryCapitalMap ["Japan"] = "东京"
    countryCapitalMap ["India"] = "新德里"
    for country,capital := range countryCapitalMap {
        fmt.Printf("国家:%s\t首都:%s\n", country, capital)
    }
    // 查看元素在集合中是否存在
    capital, ok := countryCapitalMap["American"]
    fmt.Println(ok, capital)
    // 直接读取不存在的元素会返回空
    fmt.Println(countryCapitalMap["American"])
}

结果:

国家:Italy     首都:罗马
国家:Japan     首都:东京
国家:India     首都:新德里
国家:France    首都:巴黎
false

注意:上述遍历代码多次反复执行顺序可能发生变化。对同一 map 做多次遍历的时候,每次遍历元素的次序都不相同

如果只关心每次迭代的键,可以使用下面两种方式对 map 进行遍历:

for k, _ := range m { 
  // 使用k
}

for k := range m {
  // 使用k
}

如果只关心每次迭代返回的键所对应的 value:

for _, v := range m {
  // 使用v
}

comma ok惯用法:

m := make(map[string]int)
if _, ok := m[key1]; ok {
    // "key1"在map中
}
v, ok := m["key1"]
if !ok {
    // "key1"不在map中
}
// "key1"在map中,v将被赋予"key1"键对应的value

在 Go 语言中,应使用“comma ok”惯用法对 map 进行键查找和键值读取操作。

删除数据

delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。示例如下:

package main

import "fmt"

func main() {
	countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}
	fmt.Println("原始:", countryCapitalMap)

	delete(countryCapitalMap, "France")

	// 删除后
	fmt.Println("删除France后:", countryCapitalMap)
}

结果:

原始: map[France:Paris India:New delhi Italy:Rome Japan:Tokyo]
删除后: map[India:New delhi Italy:Rome Japan:Tokyo]

delete 函数是从 map 中删除键的唯一方法。即便传给 delete 的键在 map 中并不存在,delete 函数的执行也不会失败,更不会抛出运行时的异常。

map 的排序与键值对调

map 默认是无序的,排序需要将 key(或者 value)拷贝到一个切片,再对切片排序:

package main

import (
	"fmt"
	"sort"
)

var (
	barVal = map[string]int{"alpha": 34, "bravo": 56, "charlie": 23,
		"delta": 87, "echo": 56, "foxtrot": 12,
		"golf": 34, "hotel": 16, "indio": 87,
		"juliet": 65, "kili": 43, "lima": 98}
)

func main() {
	fmt.Println("排序前:")
	for k, v := range barVal {
		fmt.Print(k, ":", v, "/")
	}
	keys := make([]string, len(barVal))
	i := 0
	for k, _ := range barVal {
		keys[i] = k
		i++
	}
	sort.Strings(keys)
	fmt.Println()
	fmt.Println("排序后:")
	for _, k := range keys {
		fmt.Print(k, ":", barVal[k], "/")
	}
}

结果:

排序前:
hotel:16/indio:87/delta:87/bravo:56/charlie:23/echo:56/foxtrot:12/golf:34/juliet:65/kili:43/alpha:34/lima:98/
排序后:
alpha:34/bravo:56/charlie:23/delta:87/echo:56/foxtrot:12/golf:34/hotel:16/indio:87/juliet:65/kili:43/lima:98/

map键值对调示例:

package main
import "fmt"

var barVal = map[string]int{"alpha": 34, "bravo": 56, "charlie": 23,
                            "delta": 87, "echo": 56, "foxtrot": 12,
                            "golf": 34, "hotel": 16, "indio": 87,
                            "juliet": 65, "kili": 43, "lima": 98}

func main() {
    invMap := make(map[int]string, len(barVal))
    for k, v := range barVal {
        invMap[v] = k
    }
    fmt.Println(invMap)
}

结果:

map[12:foxtrot 16:hotel 23:charlie 34:golf 43:kili 56:echo 65:juliet 87:indio 98:lima]

map 与并发

充当 map 描述符角色的 hmap 实例自身是有状态的(hmap.flags),对状态的读写没有并发保护,所以 map 实例不支持并发读写。如果对 map 实例进行并发读写,程序运行时就会抛出异常:

package main
import (
    "fmt"
    "time"
)
func doIteration(m map[int]int) {
    for k, v := range m {
        _ = fmt.Sprintf("[%d, %d] ", k, v)
    }
}
func doWrite(m map[int]int) {
    for k, v := range m {
        m[k] = v + 1
    }
}
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    go func() {
        for i := 0; i < 1000; i++ {
            doIteration(m)
        }
    }()
    go func() {
        for i := 0; i < 1000; i++ {
            doWrite(m)
        }
    }()
    time.Sleep(5 * time.Second)
}

报错:

fatal error: concurrent map iteration and map write

Go 1.9 版本中引入了支持并发写安全的 sync.Map 类型,支持并发读写。可以参考:http://docs.studygolang.com/pkg/sync/#Map

复合数据类型:结构体

自定义新类型

在 Go 中自定义一个新类型一般有两种方法。第一种是类型定义(Type Definition):

type T S // 定义一个新类型T

第二种是使用类型别名(Type Alias):

type T = S // type alias

在这里,S 可以是任何一个已定义的类型,包括 Go 原生类型,或者是其他已定义的自定义类型:

type T1 int 
type T2 T1

如果一个新类型是基于某个 Go 原生类型定义的,那么我们就叫 Go 原生类型为新类型的底层类型(Underlying Type)。比如这个例子中,类型 int 就是类型 T1 的底层类型。而T2 是基于 T1 类型创建的,T2 的底层类型也是类型 int。

底层类型用来判断两个类型本质上是否相同(Identical)。本质上相同的两个类型,它们的变量可以通过显式转型进行相互赋值,相反,如果本质上是不同的两个类型,它们的变量间连显式转型都不可能,更不要说相互赋值了。示例:

type T = string 
  
var s string = "hello" 
var t T = s // ok
fmt.Printf("%T\n", t) // string

因为类型 T 是通过类型别名的方式定义的,T 与 string 实际上是一个类型,所以这里,使用 string 类型变量 s 给 T 类型变量 t 赋值的动作,实质上就是同类型赋值。

类型定义也支持通过 type 块的方式进行:

type (
   T1 int
   T2 T1
   T3 string
)

结构体类型的定义

示例:

package main

import "fmt"

type Books struct {
   title string
   author string
   book_id int
}

func main() {
    // 列表形式定义,必须全部字段赋值
	book1 := Books{"Go 语言", "Robert Griesemer", 6495407}
    fmt.Println(book1)
	// 字典形式定义,被忽略的字段为 0 或 空
    book2 := Books{title: "Go 语言", book_id: 6495407}
    fmt.Println(book2)
	// 先申明再赋值
	var book3 Books
	book3.author="Guido"
	book3.title="python"
	fmt.Println(book3)
}
{Go 语言 Robert Griesemer 6495407}
{Go 语言  6495407}
{python Guido 0}

结构体定义使用 type 和 struct 关键字,格式如下:

type T struct {
    Field1 T1
    Field2 T2
    ... ...
    FieldN Tn
}

比如我们定义一本书:

package book
type Book struct {
     Title string              // 书名
     Pages int                 // 书的页数
     Indexes map[string]int    // 书的索引
}

**Go 用标识符名称的首字母大小写来判定这个标识符是否为导出标识符。**如果结构体类型只在它定义的包内使用,可以将类型名的首字母小写;如果不想将结构体类型中的某个字段暴露给其他包,同样可以把这个字段名字的首字母小写。

这样,只要其他包导入了包 book,我们就可以在这些包中直接引用类型名 Book,也可以通过 Book 类型变量引用 Name、Pages 等字段:

import ".../book"
var b book.Book
b.Title = "The Go Programming Language"
b.Pages = 800

几种特殊的定义结构体的情况。

第一种:定义一个空结构体。

空结构体就是没有包含任何字段的结构体类型:

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

var s Empty
println(unsafe.Sizeof(s)) // 0

空结构体类型变量的大小为 0,通常用于作为一种“事件”信息进行 Goroutine 之间的通信:

var c = make(chan Empty) // 声明一个元素类型为Empty的channel
c<-Empty{}               // 向channel写入一个“事件”

这种以空结构体为元素类建立的 channel,是目前能实现的、内存占用最小的 Goroutine 间通信方式。

第二种情况:使用其他结构体作为自定义结构体中字段的类型。

结构体的部分字段是另外一个结构体类型:

type Person struct {
    Name string
    Phone string
    Addr string
}
type Book struct {
    Title string
    Author Person
    ... ...
}

访问 Book 结构体字段 Author 中的 Phone 字段:

var book Book 
println(book.Author.Phone)

对于包含结构体类型字段的结构体类型来说,还支持一种叫做**嵌入字段(Embedded Field)**的方式进行定义的简便方法,无需提供字段的名字,只需要使用其类型

type Book struct {
    Title string
    Person
    ... ...
}

这种没有名字的字段可以称为匿名字段,或者把类型名看作是这个字段的名字:

var book Book 
println(book.Person.Phone) // 将类型名当作嵌入字段的名字
println(book.Phone)        // 支持直接访问嵌入字段所属类型中字段

当然,如果在在结构体类型 T 的定义中包含类型为 T 的字段:

type T struct {
    t T  
    ... ...
}

编译器就会给出“invalid recursive type T”的错误信息。

两个结构体类型 T1 与 T2 的定义存在递归的情况也是不合法的:

type T1 struct {
  t2 T2
}
type T2 struct {
  t1 T1
}

不过,虽然我们不能在结构体类型 T 定义中,拥有以自身类型 T 定义的字段,但我们却可以拥有自身类型的指针类型、以自身类型为元素类型的切片类型,以及以自身类型作为 value 类型的 map 类型的字段,比如这样:

type T struct {
    t  *T           // ok
    st []T          // ok
    m  map[string]T // ok
} 

本质是因为指针、切片、map等类型,其本质都是一个int大小的指针,因此可以确定这个结构体的大小。但是结构体里面套结构体,那么在计算该结构体占用大小的时候,就会成死循环。

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

结构体类型的变量通常都要被赋予适当的初始值后,才会有合理的意义。

零值初始化说的是使用结构体的零值作为它的初始值。结构体类型的零值变量,通常不具有或者很难具有合理的意义,比如:

var book Book // book为零值结构体变量

但如果一种类型采用零值初始化得到的零值变量是有意义的,而且是直接可用的,这种类型可以称为**“零值可用”类型**。最典型的莫过于 sync 包的 Mutex:

var mu sync.Mutex
mu.Lock()
mu.Unlock()

sync.Mutex 结构体的零值状态被设计为可用状态,这样开发者便可直接基于零值状态下的 Mutex 进行 lock 与 unlock 操作,不需要额外显式地对它进行初始化操作。

bytes.Buffer 结构体类型,也是一个零值可用类型的典型例子:

var b bytes.Buffer
b.Write([]byte("Hello, Go"))
fmt.Println(b.String()) // 输出:Hello, Go

对结构体变量进行显式初始化的方式有两种,格式如下:

variable_name := structure_variable_type {value1, value2...valuen}
或
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

按顺序依次给每个结构体字段进行赋值:

type Book struct {
    Title string              // 书名
    Pages int                 // 书的页数
    Indexes map[string]int    // 书的索引
}
var book = Book{"The Go Programming Language", 700, make(map[string]int)}

一旦结构体中包含非导出字段,那么这种逐一字段赋值的方式就不再被支持了,编译器会报错:

type T struct {
    F1 int
    F2 string
    f3 int
    F4 int
    F5 int
}
var t = T{11, "hello", 13} // 错误:implicit assignment of unexported field 'f3' in T literalvar t = T{11, "hello", 13, 14, 15} // 错误:implicit assignment of unexported field 'f3' in T literal

推荐用field:value形式的对结构体类型变量进行显式初始化:

var t = T{
    F2: "hello",
    F1: 11,
    F4: 14,
}

使用特定的构造函数创建结构体

使用特定的构造函数创建并初始化结构体变量的例子,在 Go 标准库中就有很多,其中 time.Timer 这个结构体就是一个典型的例子:

// $GOROOT/src/time/sleep.go
type runtimeTimer struct {
    pp       uintptr
    when     int64
    period   int64
    f        func(interface{}, uintptr) 
    arg      interface{}
    seq      uintptr
    nextwhen int64
    status   uint32
}
type Timer struct {
    C <-chan Time
    r runtimeTimer
}

imer 结构体中包含了一个非导出字段 r,r 的类型为另外一个结构体类型 runtimeTimer。Go 标准库提供了一个 Timer 结构体专用的构造函数 NewTimer,它的实现如下:

// $GOROOT/src/time/sleep.go
func NewTimer(d Duration) *Timer {
    c := make(chan Time, 1)
    t := &Timer{
        C: c,
        r: runtimeTimer{
            when: when(d),
            f:    sendTime,
            arg:  c,
        },
    }
    startTimer(&t.r)
    return t
}

NewTimer 这个函数只接受一个表示定时时间的参数 d,在经过一个复杂的初始化过程后,它返回了一个处于可用状态的 Timer 类型指针实例。

专用构造函数大多都符合这种模式:

func NewT(field1, field2, ...) *T {
    ... ...
}

结构体的类型嵌入

带有嵌入字段(Embedded Field)的结构体定义:

type T1 int
type t2 struct{
    n int
    m int
}
type I interface {
    M1()
}
type S1 struct {
    T1
    *t2
    I            
    a int
    b string
}

T1、t2 和 I 既代表字段的名字,也代表字段的类型。例如 t2 表示字段名为 t2,类型为 t2 的指针类型。

这些字段被叫做嵌入字段(Embedded Field)。看看一个例子:

type MyInt int

func (n *MyInt) Add(m int) {
    *n = *n + MyInt(m)
}

type t struct {
    a int
    b int
}

type S struct {
    *MyInt
    t
    io.Reader
    s string
    n int
}

func main() {
    m := MyInt(17)
    r := strings.NewReader("hello, go")
    s := S{
        MyInt: &m,
        t: t{
            a: 1,
            b: 2,
        },
        Reader: r,
        s:      "demo",
    }
    var sl = make([]byte, len("hello, go"))
    s.Reader.Read(sl)
    fmt.Println(string(sl)) // hello, go
    s.MyInt.Add(5)
    fmt.Println(*(s.MyInt)) // 22
}

嵌入字段的可见性与嵌入字段的类型的可见性是一致的。如果嵌入类型的名字是首字母大写的,那么也就说明这个嵌入字段是可导出的。

注意:嵌入字段类型的底层类型不能为指针类型,嵌入字段的名字在结构体定义必须唯一。

s.Reader.Read 和 s.MyInt.Add调用了嵌入字段的方法,其实嵌入字段的方法也可以直接被调用:

var sl = make([]byte, len("hello, go"))
s.Read(sl) 
fmt.Println(string(sl))
s.Add(5) 
fmt.Println(*(s.MyInt))

当调用 Read 方法时,Go 发现结构体类型 S 自身并没有定义 Read 方法,于是 Go 会查看 S 的嵌入字段对应的类型是否定义了 Read 方法,于是 s.Read 的调用就被转换为 s.Reader.Read 调用。

接口

Go 语言的接口将共性的方法定义在一起,与其他面向对象语言不同的是,其他类型只要实现了这些方法就是实现了这个接口。

示例:

package main

import "fmt"

// 定义接口
type Phone interface {
    call()
}
// 定义结构体
type NokiaPhone struct {
}
// 实现接口方法
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}
// 定义结构体
type IPhone struct {
}
// 实现接口方法
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()
}

结果:

I am Nokia, I can call you!
I am iPhone, I can call you!

接口类型的类型嵌入

接口类型间的嵌入与结构体类型间的嵌入很相似,只要把一个接口类型的名称直接写到另一个接口类型的成员列表中就可以了。比如:

type Animal interface {
	ScientificName() string
	Category() string
}
 
type Pet interface {
	Animal
	Name() string
}

接口类型Pet,它的方法集合中包含了三个方法 ScientificName、Category 和 Name。用接口类型 Animal 替代接口类型 Pet 定义中的 ScientificName 和 Category。接口类型嵌入就是新接口类型(如接口类型 I)将嵌入的接口类型(如接口类型 E)的方法集合,并入到自己的方法集合中

Go 语言团队鼓励我们声明体量较小的接口,并建议我们通过这种接口间的组合来扩展程序、增加程序的灵活性。

比如,io 包的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌入 Reader、Writer 或 Closer 三个基本的接口类型组合而成的。下面是io 包 Reader、Writer 和 Closer 的定义:

// $GOROOT/src/io/io.go
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}

而 io 包的 ReadWriter、ReadWriteCloser 等接口类型就是通过嵌入上面基本接口类型组合而形成:

type ReadWriter interface {
    Reader
    Writer
}
type ReadCloser interface {
    Reader
    Closer
}
type WriteCloser interface {
    Writer
    Closer
}
type ReadWriteCloser interface {
    Reader
    Writer
    Closer
}

在 Go 1.14 版本之前,嵌入的接口类型的方法集合不能有交集,同时嵌入的接口类型的方法集合中的方法名字,也不能与新接口中的其他方法同名。

结构体类型中嵌入接口类型

结构体类型的方法集合,包含嵌入的接口类型的方法集合:

type I interface {
    M1()
    M2()
}
type T struct {
    I
}
func (T) M3() {}

由于 *T 类型方法集合包括 T 类型的方法集合,因此无论是类型 T 还是类型 *T,它们的方法集合都包含 M1、M2 和 M3。

当结构体嵌入的多个接口类型的方法集合存在交集时,编译器可能会出现的错误提示。前面说的是接口类型中嵌入接口类型的情况,这里说的是在结构体类型中嵌入方法集合有交集的接口类型。Go 编译器会因无法确定使用哪个方法而报错:

type E1 interface {
      M1()
      M2()
      M3()
  }
  
  type E2 interface {
     M1()
     M2()
     M4()
 }
 
 type T struct {
     E1
     E2
 }
 
 func main() {
     t := T{}
     t.M1()
     t.M2()
 }

此时编辑器报错:

Ambiguous reference 'M1'
Ambiguous reference 'M2'

要解决这个问题除了消除 E1 和 E2 方法集合存在交集的情况外,还可以为T增加 M1 和 M2 方法的实现:

func (T) M1() { println("T's M1") }
func (T) M2() { println("T's M2") }
func main() {
    t := T{}
    t.M1() // T's M1
    t.M2() // T's M2
}

嵌入某接口类型的结构体类型的方法集合包含了这个接口类型的方法集合,这就意味着,这个结构体类型也是它嵌入的接口类型的一个实现,即便结构体类型自身并没有实现这个接口类型的任意一个方法。利用这个特性,结构体类型嵌入接口类型可以简化单元测试的编写。

结构体类型中嵌入结构体类型

在结构体类型中嵌入结构体类型,外部的结构体类型 T 可以“继承”嵌入的结构体类型的所有方法的实现。

type T1 struct{}

func (T1) T1M1()   { println("T1's M1") }
func (*T1) PT1M2() { println("PT1's M2") }

type T2 struct{}

func (T2) T2M1()   { println("T2's M1") }
func (*T2) PT2M2() { println("PT2's M2") }

type T struct {
	T1
	*T2
}

func main() {
	t := T{
		T1: T1{},
		T2: &T2{},
	}
	dumpMethodSet(t)
	dumpMethodSet(&t)
}

运行结果:

main.T's method set:
- PT2M2
- T1M1
- T2M1

*main.T's method set:
- PT1M2
- PT2M2
- T1M1
- T2M1

可以看到:

  • 类型 T 的方法集合 = T1 的方法集合 + *T2 的方法集合
  • 类型 *T 的方法集合 = *T1 的方法集合 + *T2 的方法集合

注意:类型别名(type alias)定义的新类型能够“继承”原类型的方法集合,但通过类型声明定义 defined 类型相当于创建了新类型不会继承原 defined 类型的方法集合。

// 类型别名
type T1 = T
// 类型声明
type T1 T

多态

示例:

package main

import "fmt"

type Shaper interface {
    Area() float32
}

type Square struct {
    side float32
}

func (sq *Square) Area() float32 {
    return sq.side * sq.side
}

func main() {
    sq1 := new(Square)
    sq1.side = 5
    var areaIntf Shaper
    areaIntf = sq1
    fmt.Println(areaIntf.Area())
}

结果:25.000000

上述代码将一个 Square 类型的变量赋值给一个接口类型的变量:areaIntf = sq1 。现在接口变量包含一个指向 Square 变量的引用,通过它可以调用 Square 上的方法 Area()

这是 多态 的 Go 版本。如果 Square 没有实现 Area() 方法,编译器将会给出清晰的错误信息:

cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)

如果 Shaper 有另一个方法 Perimeter()Square 没有实现它,即使没在 Square 实例上调用这个方法,编译器也会给出上面同样的错误。

类型 Rectangle 也实现了 Shaper 接口。接着创建一个 Shaper 类型的数组,迭代它的每一个元素并在上面调用 Area() 方法,以此来展示多态行为:

package main

import "fmt"

type Shaper interface {
    Area() float32
}

type Square struct {
    side float32
}

func (sq *Square) Area() float32 {
    return sq.side * sq.side
}

type Rectangle struct {
    length, width float32
}

func (r Rectangle) Area() float32 {
    return r.length * r.width
}

func main() {
    r := Rectangle{5, 3}
    q := &Square{5}
    shapes := []Shaper{r, q}
    for _, v := range shapes {
        fmt.Println(v, v.Area())
    }
}

输出:

{5 3} 15
&{5} 25

Square实用时使用了指针类型接收变量,使用时也必须使用其指针作为Shaper类型。

io 包里有一个接口类型 Reader:

type Reader interface {
    Read(p []byte) (n int, err error)
}

定义变量 rvar r io.Reader,可以写如下的代码:

    var r io.Reader
    r = os.Stdin    // see 12.1
    r = bufio.NewReader(r)
    r = new(bytes.Buffer)
    f,_ := os.Open("test.txt")
    r = bufio.NewReader(f)

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

比如接口 File 包含了 ReadWriteLock 的所有方法,它还额外有一个 Close() 方法。

type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock()
}

type File interface {
    ReadWrite
    Lock
    Close()
}

检测和转换接口变量的类型

对于接口变量 varI ,可以使用类型断言 来判断 varI 是否包含类型 T 的值:

v := varI.(T)

varI 必须是一个接口变量,否则编译器会报错:invalid type assertion: varI.(T) (non-interface type (type of varI) on left)

最好使用if语句判断:

if v, ok := varI.(T); ok {  // checked type assertion
    Process(v)
    return
}
// varI is not of type T

如果转换合法,vvarI 转换到类型 T 的值,ok 会是 true;否则 v 是类型 T 的零值,okfalse,也没有运行时错误发生。

例如:

package main

import (
    "fmt"
    "math"
)

type Square struct {
    side float32
}

type Circle struct {
    radius float32
}

type Shaper interface {
    Area() float32
}

func (sq *Square) Area() float32 {
    return sq.side * sq.side
}

func (ci *Circle) Area() float32 {
    return ci.radius * ci.radius * math.Pi
}

func main() {
    sq1 := new(Square)
    sq1.side = 5

    var areaIntf Shaper = sq1
    if t, ok := areaIntf.(*Square); ok {
        fmt.Printf("类型为: %T\n", t)
    }
    if u, ok := areaIntf.(*Circle); ok {
        fmt.Printf("类型为: %T\n", u)
    } else {
        fmt.Println("不是Circle类型")
    }
}

输出:

类型为: *main.Square
不是Circle类型

必须使用指针变量检查。

Type-Switch语句

Type-Switch语句用来判断某个 interface 变量的变量类型,语法格式如下:

switch x.(type){
    case type1:
       statement(s);      
    case type2:
       statement(s);
    ...
    default: /* 可选 */
       statement(s);
}

示例:

package main

import (
	"fmt"
	"math"
)

type Square struct {
	side float32
}

type Circle struct {
	radius float32
}

type Shaper interface {
	Area() float32
}

func (sq *Square) Area() float32 {
	return sq.side * sq.side
}

func (ci *Circle) Area() float32 {
	return ci.radius * ci.radius * math.Pi
}

func main() {
	sq1 := new(Square)
	sq1.side = 5
	var areaIntf Shaper = sq1
	switch v := areaIntf.(type) {
	case *Square:
		fmt.Printf("Square,类型:%T,值:%v\n", v, v)
	case *Circle:
		fmt.Printf("Circle,类型:%T,值:%v\n", v, v)
	case nil:
		fmt.Printf("空类型\n")
	default:
		fmt.Printf("未知类型:%T\n", v)
	}
}

执行结果为:

Square,类型:*main.Square,值:&{5}

type-switch 不允许有 fallthrough

空接口

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

type Any interface {}

空接口类似 Java/C# 中所有类的基类: Object 类,可以给一个空接口类型的变量 var val interface {} 赋任何类型的值。

一对不包裹任何东西的花括号,除了可以代表空的代码块之外,还可以用于表示不包含任何内容的数据结构(或者说数据类型)。

示例:

package main
import "fmt"

type Person struct {
    name string
    age  int
}

type Any interface{}

func main() {
    var val Any
    val = 5
    fmt.Println("val =", val)
    val = "ABC"
    fmt.Println("val =", val)
    pers1 := new(Person)
    pers1.name = "小华"
    pers1.age = 25
    val = pers1
    fmt.Println("val =", val)
    switch t := val.(type) {
    case int:
        fmt.Printf("int类型 %T\n", t)
    case string:
        fmt.Printf("string类型 %T\n", t)
    case bool:
        fmt.Printf("boolean类型 %T\n", t)
    case *Person:
        fmt.Printf("Person指针类型 %T\n", t)
    default:
        fmt.Printf("未知类型 %T", t)
    }
}

输出:

val = 5
val = ABC
val = &{Rob Pike 55}
Person指针类型 *main.Person

利用空接口可以很方便的判断指定接口的类型:

package main

import "fmt"

type specialString string

func TypeSwitch(any interface{}) {
	switch v := any.(type) {
	case bool:
		fmt.Println("bool 类型", v)
	case int:
		fmt.Println("int 类型", v)
	case float32:
		fmt.Println("float32 类型", v)
	case string:
		fmt.Println("string 类型", v)
	case specialString:
		fmt.Println("special String!", v)
	default:
		fmt.Println("未知类型!")
	}
}

func main() {
	var whatIsThis specialString = "hello"
	TypeSwitch(whatIsThis)
}

反射

反射包的Type用来表示一个Go类型,反射包的Value为Go值提供了反射接口。

两个简单的函数,reflect.TypeOfreflect.ValueOf,返回被检查对象的类型和值:

func TypeOf(i interface{}) Type
func ValueOf(i interface{}) Value

示例:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4
	fmt.Println(reflect.TypeOf(x))
	v := reflect.ValueOf(x)
	fmt.Printf("value:%v,type:%v,kind:%v,Float:%v\n", v, v.Type(), v.Kind(), v.Float())
	fmt.Printf("Interface:%v\n", v.Interface(),)
	fmt.Println(v.Interface().(float64))
}

结果:

float64
value:3.4,type:float64,kind:float64,Float:3.4
Interface:3.4
3.4

Kind总是返回底层类型,Type返回直接类型。

Interface()方法还原接口的值

知道x是一个float64类型的值,reflect.ValueOf(x).float()返回这个float64类型的实际值;同样的适用于Int(), Bool(), Complex() ,String()

**通过反射设置值:**前面通过值传递的方法是无法通过反射设置值的,必须通过引用传递并使用Elem()函数才可行。对于反射出来的值或字段可以使用CanSet()方法测试是否可设置。

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var x float64 = 3.4
	v := reflect.ValueOf(x)
	fmt.Println(v.Type(), v.CanSet(), v.Interface())
	// v.SetFloat(3.1415) // 报错: panic: reflect: reflect.Value.SetFloat using unaddressable value
	v = reflect.ValueOf(&x).Elem()
	fmt.Println(v.Type(), v.CanSet(), v.Interface())
	v.SetFloat(3.1415)
	fmt.Println(v.Interface())
}

结果:

float64 false 3.4
float64 true 3.4
3.1415

反射struct:有些时候需要反射一个结构类型。NumField()方法返回结构内的字段数量;可以通过一个for循环通过索引取得每个字段的值Field(i)

示例:

package main

import (
	"fmt"
	"reflect"
)

type NotknownType struct {
	s1, s2, s3 string
}

func (n NotknownType) String() string {
	return n.s1 + " - " + n.s2 + " - " + n.s3
}

var secret interface{} = NotknownType{"Ada", "Go", "Oberon"}

func main() {
	value := reflect.ValueOf(secret)
	fmt.Println(value.Type(), value.Kind())
	for i := 0; i < value.NumField(); i++ {
		fmt.Printf("Field %d: %v\n", i, value.Field(i))
	}
	results := value.Method(0).Call(nil)
	fmt.Println(results)
}

输出:

main.NotknownType struct
Field 0: Ada
Field 1: Go
Field 2: Oberon
[Ada - Go - Oberon]

对于struct反射出来的字段同样需要传递引用并使用Elem()函数才可以被设置:

package main

import (
	"fmt"
	"reflect"
)

type T struct {
	A int
	B string
}

func main() {
	t := T{23, "skidoo"}
	fmt.Println("修改前:", t)
	s := reflect.ValueOf(&t).Elem()
    fmt.Println(s.Type(), s.CanSet(), s.Interface())
	typeOfT := s.Type()
	for i := 0; i < s.NumField(); i++ {
		f := s.Field(i)
		fmt.Printf("%d,变量名:%s,类型:%s,值:%v\n",
			i, typeOfT.Field(i).Name, f.Type(), f.Interface())
	}
	s.Field(0).SetInt(77)
	s.Field(1).SetString("Strip")
	fmt.Println("修改后:", t)
}

输出:

修改前: {23 skidoo}
main.T true {23 skidoo}
0,变量名:A,类型:int,值:23
1,变量名:B,类型:string,值:skidoo
修改后: {77 Strip}

并发

goroutine 轻量级线程

通过 go 关键字执行函数即可开启 goroutine 轻量级线程。例如:

go f(x, y, z)

示例:

package main

import (
	"fmt"
	"time"
	"bufio"
	"os"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s, i)
	}
}

func main() {
	go say("world")
	go say("go")
	say("hello")
	bufio.NewReader(os.Stdin).ReadString('\n')
}

结果:

go 0
world 0
hello 0
go 1
hello 1
world 1
hello 2
go 2
world 2
hello 3
go 3
world 3
hello 4
world 4
go 4

通道(channel)

通道(channel)可用于两个 goroutine 之间的通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据并把值赋给 v

使用chan关键字即可声明一个通道,但通道在使用前必须先创建:

ch := make(chan int)

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:

package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 把 sum 发送到通道 c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // 从通道 c 中接收

	fmt.Println(x, y, x+y)
}

输出结果为:

-5 17 12

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

带缓冲区的通道发送端发送的数据会放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。但缓冲区塞满时,数据发送端就无法再继续发送数据。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,接收方在有值可以接收之前会一直阻塞。

实例:

package main

import "fmt"

func main() {
	// 这里我们定义了一个可以存储整数类型的带缓冲通道
	// 缓冲区大小为2
	ch := make(chan int, 2)

	// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
	// 而不用立刻需要去同步读取数据
	ch <- 1
	ch <- 2

	// 获取这两个数据
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

执行输出结果为:

1
2

通过 range 关键字可以遍历通道,格式如下:

v, ok := <-ch

如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

package main

import "fmt"

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(10, c)
	// 若c 通道不关闭,range 循环会一直阻塞
    for i := range c {
        fmt.Print(i,", ")
    }
}

执行结果:

0, 1, 1, 2, 3, 5, 8, 13, 21, 34,

select 语句

select 随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的:

  • 每个 case 都必须是一个通信

  • 所有 channel 表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果任意某个通信可以进行,它就执行,其他被忽略。

  • 如果有多个 case 都可以运行,Select 会随机公平地选出一个执行。其他不会执行。

    否则:

    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通信可以运行;Go 不会重新对 channel 或值进行求值。

示例:

package main

import "fmt"

func main() {
   var c1, c2, c3 chan int
   var i1, i2 int
   select {
      case i1 = <-c1:
         fmt.Println("从c1通道接收到", i1)
      case c2 <- i2:
         fmt.Println("向c2通道发送", i2)
      case i3, ok := <-c3:
         if ok {
            fmt.Println("从c3通道接收到 ", i3,)
         } else {
            fmt.Println("c3通道已关闭")
         }
      default:
         fmt.Println("没有通信")
   }
}

sync.Mutex与sync.RWMutex

sync表示同步,虽然Go 语言宣扬的“用通讯的方式共享数据”,但通过共享数据的方式来传递信息和协调线程运行的做法其实更加主流。

一旦数据被多个线程共享,那么就很可能会产生争用和冲突的情况。这种情况也被称为竞态条件(race condition)

比如同时有多个线程连续向同一个缓冲区写入数据块:

package main

import (
	"bytes"
	"fmt"
	"io"
	"io/ioutil"
	"sync"
)

func main() {
	var buffer bytes.Buffer
	// mu 代表以下流程要使用的互斥锁。
	var mu sync.Mutex
	// sign 代表信号的通道。
	sign := make(chan struct{}, 3)

	for i := 1; i <= 3; i++ {
		go func(id int, writer io.Writer) {
			defer func() {
				sign <- struct{}{}
			}()
			for j := 1; j <= 3; j++ {
				data := fmt.Sprintf("\n[id: %d, iteration: %d]", id, j)
				mu.Lock()
				_, err := writer.Write([]byte(data))
				mu.Unlock()
				if err != nil {
					fmt.Printf("error: %s [%d]", err, id)
				}
			}
		}(i, &buffer)
	}
	for i := 0; i < 3; i++ {
		<-sign
	}
	data, err := ioutil.ReadAll(&buffer)
	if err != nil {
		fmt.Println("读取出错:", err)
	}
	content := string(data)
	fmt.Println("读取结果:", content)
}

输出:

读取结果: 
[id: 3, iteration: 1]
[id: 3, iteration: 2]
[id: 1, iteration: 1]
[id: 1, iteration: 2]
[id: 1, iteration: 3]
[id: 3, iteration: 3]
[id: 2, iteration: 1]
[id: 2, iteration: 2]
[id: 2, iteration: 3]

上述代码使用了同步工具sync包中的Mutex互斥锁。一个互斥锁可以保证在同一时刻只有一个 goroutine 处于该临界区之内。

读写锁:

除了互斥锁外,还有读写锁sync.RWMutex,读写锁是把对共享资源的“读操作”和“写操作”区别对待了。sync.RWMutex类型中的Lock方法和Unlock方法分别用于对写锁进行锁定和解锁,而它的RLock方法和RUnlock方法则分别用于对读锁进行锁定和解锁。

对于某个受到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行。

示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

// counter 代表计数器。
type counter struct {
	num uint         // 计数。
	mu  sync.RWMutex // 读写锁。
}

// number 会返回当前的计数。
func (c *counter) number() uint {
	c.mu.RLock()
	defer c.mu.RUnlock()
	return c.num
}

// add 会增加计数器的值,并会返回增加后的计数。
func (c *counter) add(increment uint) uint {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.num += increment
	return c.num
}

func main() {
	c := counter{}
	count(&c)
}

func count(c *counter) {
	// sign 用于传递演示完成的信号。
	sign := make(chan struct{}, 3)
	go func() { // 用于增加计数。
		defer func() {
			sign <- struct{}{}
		}()
		// 每0.5秒计数+1
		for i := 1; i <= 5; i++ {
			time.Sleep(time.Millisecond * 500)
			c.add(1)
		}
	}()
	go func() {
		defer func() {
			sign <- struct{}{}
		}()
		// 每0.2秒读取一次计数器的值
		for j := 1; j <= 10; j++ {
			time.Sleep(time.Millisecond * 200)
			fmt.Printf("线程1|当前计数器的值:%v,j:%v\n", c.number(), j)
		}
	}()
	go func() {
		defer func() {
			sign <- struct{}{}
		}()
		// 每0.3秒读取一次计数器的值
		for k := 1; k <= 10; k++ {
			time.Sleep(time.Millisecond * 300)
			fmt.Printf("线程2|当前计数器的值:%v,k:%v\n", c.number(), k)
		}
	}()
	<-sign
	<-sign
	<-sign
}

输出:

线程1|当前计数器的值:0,j:1
线程2|当前计数器的值:0,k:1
线程1|当前计数器的值:0,j:2
线程2|当前计数器的值:1,k:2
线程1|当前计数器的值:1,j:3
线程1|当前计数器的值:1,j:4
线程2|当前计数器的值:1,k:3
线程1|当前计数器的值:1,j:5
线程1|当前计数器的值:2,j:6
线程2|当前计数器的值:2,k:4
线程1|当前计数器的值:2,j:7
线程2|当前计数器的值:2,k:5
线程1|当前计数器的值:3,j:8
线程2|当前计数器的值:3,k:6
线程1|当前计数器的值:3,j:9
线程1|当前计数器的值:4,j:10
线程2|当前计数器的值:4,k:7
线程2|当前计数器的值:4,k:8
线程2|当前计数器的值:5,k:9
线程2|当前计数器的值:5,k:10

上述程序使用一个线程修改计数器,两个线程读取计数器的值,所以使用了读锁。

条件变量sync.Cond

锁对象可以获取条件变量,它提供了三个方法:等待通知(wait)、单发通知(signal)和广播通知(broadcast)。

获取示例:

var mailbox uint8
var lock sync.RWMutex
sendCond := sync.NewCond(&lock)
recvCond := sync.NewCond(lock.RLocker())

lock读写锁,基于这把锁,由sync.NewCond函数获取了两个条件变量分别为sendCondrecvCond

sendCond用于监视共享资源的写操作,recvCond变量用于监视共享资源的读操作。

mailbox则用于标记锁的读写状态。发送者的使用示例:

lock.Lock()
for mailbox == 1 {
    sendCond.Wait()
}
mailbox = 1
lock.Unlock()
recvCond.Signal()

接收者:

lock.RLock()
for mailbox == 0 {
	recvCond.Wait()
}
mailbox = 0
lock.RUnlock()
sendCond.Signal()

完整示例:

package main

import (
   "log"
   "sync"
   "time"
)

func main() {
   // mailbox 代表信箱。
   // 0代表信箱是空的,1代表信箱是满的。
   var mailbox uint8
   // lock 代表信箱上的锁。
   var lock sync.RWMutex
   // sendCond 代表专用于发信的条件变量。
   sendCond := sync.NewCond(&lock)
   // recvCond 代表专用于收信的条件变量。
   recvCond := sync.NewCond(lock.RLocker())

   // sign 用于传递演示完成的信号。
   sign := make(chan struct{}, 3)
   max := 5
   go func(max int) { // 用于发信。
      defer func() {
         sign <- struct{}{}
      }()
      for i := 1; i <= max; i++ {
         time.Sleep(time.Millisecond * 500)
         lock.Lock()
         for mailbox == 1 {
            sendCond.Wait()
         }
         log.Printf("sender [%d]: the mailbox is empty.", i)
         mailbox = 1
         log.Printf("sender [%d]: the letter has been sent.", i)
         lock.Unlock()
         recvCond.Signal()
      }
   }(max)
   go func(max int) { // 用于收信。
      defer func() {
         sign <- struct{}{}
      }()
      for j := 1; j <= max; j++ {
         time.Sleep(time.Millisecond * 500)
         lock.RLock()
         for mailbox == 0 {
            recvCond.Wait()
         }
         log.Printf("receiver [%d]: the mailbox is full.", j)
         mailbox = 0
         log.Printf("receiver [%d]: the letter has been received.", j)
         lock.RUnlock()
         sendCond.Signal()
      }
   }(max)

   <-sign
   <-sign
}

原子操作(atomic operation)

sync/atomic包提供的原子操作有:加法(add)、比较并交换(compare and swap,即 CAS)、加载(load)、存储(store)和交换(swap)。

支持的数据类型有:int32int64uint32uint64uintptr,以及unsafe包中的Pointer。不过未提供对unsafe.Pointer的原子加法操作的函数。

原子减法

对于int32我们可以直接进行原子减法:

num1 := int32(18)
atomic.AddInt32(&num1, int32(-3))
fmt.Println("num1 =", num1)

输出:15

但对于uint32类型值做原子减法就会麻烦一些,需要先把差量转换为有符号的int32类型再转换为uint32,用表达式来描述就是uint32(int32(-3)),不过编译器会报错常量-3不在uint32类型可表示的范围内”。但如果我们把int32(-3)的结果先赋给变量,再将该变量的值转换为uint32类型的值,就可以绕过编译器的检查并得到正确的结果了。

示例:

num2 := uint32(18)
delta := int32(-3)
atomic.AddUint32(&num2, uint32(delta))
fmt.Println("num2 =", num2)

输出:15

还有一种方式是根据补码进行计算,补码的定义是将其原码除符号位外的所有位取反后加1,等价于^uint32(-N-1))。(^表示取反)

验证一下:

delta := int32(-3)
fmt.Printf("补码分别为:\n%b\n%b\n", uint32(delta), ^uint32(-delta-1))

输出:

补码分别为:
11111111111111111111111111111101
11111111111111111111111111111101

那么我们还可以这样写:

num3 := uint32(18)
atomic.AddUint32(&num3, ^uint32(3-1))
fmt.Println("num3= ", num3)

输出:15

比较并交换操作与交换操作的区别

比较并交换:CAS 操作,在条件满足的情况下才会进行值的交换。

交换:swap操作,把新值赋给变量,并返回变量的旧值。

进行 CAS 操作时,函数会先判断被操作的变量是否等于预期值,满足则赋值并返回true;否则忽略操作并返回false。

CAS 操作是一种操作组合将它与for语句联用就可以实现一种简易的自旋锁(spinlock):

for !atomic.CompareAndSwapInt32(&num, 5, 0) {
	time.Sleep(time.Millisecond * 500)
}

上述代码会不断检查num的值,直到num等于5时将其修改为0。for语句加 CAS 操作的假设往往是:共享资源状态的改变并不频繁,或它的状态总会变成期望的那样。

使用CAS 操作写数据时,读数据时依然需要使用原子操作,否则仅读到仅修改了一部分的值,破坏了值的完整性。

由于原子操作函数的执行速度要比互斥锁快得多,所以再确定某个场景下可以使用原子操作函数时,就不要再考虑互斥锁了。不过原子操作函数只支持非常有限的数据类型,互斥锁应用更广泛。

简易自旋锁示例:

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

// 简易的自旋锁
func main() {
	sign := make(chan struct{}, 2)
	num := int32(0)
	fmt.Println("num =", num)
	go func() { // 定时增加num的值。
		defer func() {
			sign <- struct{}{}
		}()
		for newNum := num; newNum != 5; {
			newNum = atomic.AddInt32(&num, 1)
			fmt.Println("newNum =", newNum)
			time.Sleep(time.Millisecond * 500)
		}
	}()
	go func() { // 定时检查num的值,如果等于10就将其归零。
		defer func() {
			sign <- struct{}{}
		}()
		for !atomic.CompareAndSwapInt32(&num, 5, 0) {
			time.Sleep(time.Millisecond * 500)
		}
		fmt.Println("变量已归0")
	}()
	<-sign
	<-sign
}

LoadInt32操作示例:

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

func main() {
	coordinateWithChan()
}

func coordinateWithChan() {
	sign := make(chan struct{}, 2)
	num := int32(0)
	fmt.Println("num =", num)
	max := int32(5)
	go addNum(&num, 1, max, func() {
		sign <- struct{}{}
	})
	go addNum(&num, 2, max, func() {
		sign <- struct{}{}
	})
	<-sign
	<-sign
}

// addNum 用于原子地增加numP所指的变量的值。
func addNum(numP *int32, id, max int32, deferFunc func()) {
	defer func() {
		deferFunc()
	}()
	for i := 0; ; i++ {
		currNum := atomic.LoadInt32(numP)
		if currNum >= max {
			break
		}
		newNum := currNum + 1
		time.Sleep(time.Millisecond * 200)
		if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
			fmt.Printf("id=%d,i=%d,newNum=%d\n", id, i, newNum)
		} else {
			fmt.Printf("id=%d,i=%d,newNum=%d操作失败\n", id, i, newNum)
		}
	}
}

输出:

num = 0
id=1,i=0,newNum=1
id=2,i=0,newNum=1 CAS操作失败
id=2,i=1,newNum=2 CAS操作失败
id=1,i=1,newNum=2
id=1,i=2,newNum=3
id=2,i=2,newNum=3 CAS操作失败
id=2,i=3,newNum=4
id=1,i=3,newNum=4 CAS操作失败
id=2,i=4,newNum=5 CAS操作失败
id=1,i=4,newNum=5

sync包的WaitGroupOnce

前面我们使用通道来阻塞主线程等待子线程完成,但是不太优雅。sync包的WaitGroup则更适用一些。

WaitGroup类型拥有三个指针方法:AddDoneWait。你可以想象该类型中有一个计数器,它的默认值是0

Done方法对计数器的值进行减一操作,Wait方法阻塞当前的 goroutine,直到其所属值中的计数器归零。

使用sync.WaitGroup可以将前面的例子的coordinateWithChan方法替换为如下写法:

func coordinateWithChan() {
	var wg sync.WaitGroup
	wg.Add(2)
	num := int32(0)
	fmt.Println("num =", num)
	max := int32(5)
	go addNum(&num, 1, max, wg.Done)
	go addNum(&num, 2, max, wg.Done)
	wg.Wait()
}

WaitGroup的一个计数周期:

image-20211218191231109

如果WaitGroup的Wait方法被执行期间,跨越了两个计数周期,那么就会引发一个 panic

分批启用执行子任务的 goroutine,最简单的方式就是使用for循环来作为辅助。示例:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	coordinateWithWaitGroup()
}

func coordinateWithWaitGroup() {
	total := 12
	stride := 3
	var num int32
	fmt.Printf("num = %d [with sync.WaitGroup]\n", num)
	var wg sync.WaitGroup
	for i := 1; i <= total; i = i + stride {
		wg.Add(stride)
		for j := 0; j < stride; j++ {
			go addNum(&num, i+j, wg.Done)
		}
		wg.Wait()
	}
	fmt.Println("End.")
}

// addNum 用于原子地增加一次numP所指的变量的值。
func addNum(numP *int32, id int, deferFunc func()) {
	defer func() {
		deferFunc()
	}()
	for i := 0; ; i++ {
		currNum := atomic.LoadInt32(numP)
		newNum := currNum + 1
		time.Sleep(time.Millisecond * 200)
		if atomic.CompareAndSwapInt32(numP, currNum, newNum) {
			fmt.Printf("id=%d,i=%d,newNum=%d\n", id, i, newNum)
			break
		} else {
			fmt.Printf("id=%d,i=%d,CAS操作失败\n", id, i)
		}
	}
}

sync.Once使用起来会更加简单,Once类型的Do方法接收一个函数,将会启动一个子线程去执行该函数,主函数也将持续等待子线程执行完毕。示例:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var counter uint32
	var once sync.Once
	once.Do(func() {
		for i := 0; i < 5; i++ {
			atomic.AddUint32(&counter, 1)
			time.Sleep(time.Millisecond * 100)
		}
	})
	fmt.Println(counter)
	once.Do(func() {
		for i := 0; i < 5; i++ {
			atomic.AddUint32(&counter, 2)
			time.Sleep(time.Millisecond * 100)
		}
	})
	fmt.Println(counter)
}

有多个 goroutine 并发地调用了同一个Once值的Do方法时,永远只会执行第一次被调用时传入的参数函数,其他的 goroutine 都会被阻塞在锁定该Once值的互斥锁m的那行代码上,在第一个goroutine 执行完毕后才会被唤醒并结束。示例:

package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var once sync.Once
	var wg sync.WaitGroup
	wg.Add(3)
	go func() {
		defer wg.Done()
		once.Do(func() {
			for i := 0; i < 3; i++ {
				fmt.Printf("Do task. [1-%d]\n", i)
				time.Sleep(time.Second)
			}
		})
		fmt.Println("Done. [1]")
	}()
	go func() {
		defer wg.Done()
		time.Sleep(time.Millisecond * 500)
		once.Do(func() {
			fmt.Println("Do task. [2]")
		})
		fmt.Println("Done. [2]")
	}()
	go func() {
		defer wg.Done()
		time.Sleep(time.Millisecond * 500)
		once.Do(func() {
			fmt.Println("Do task. [3]")
		})
		fmt.Println("Done. [3]")
	}()
	wg.Wait()
}

结果:

Do task. [1-0]
Do task. [1-1]
Do task. [1-2]
Done. [1]
Done. [2]
Done. [3]

从执行结果可以看到,多个 goroutine 并发地调用了同一个Once值的Do方法时,只有第一个被执行。

context.Context类型

前面的coordinateWithWaitGroup函数使用sync.WaitGroup实现了同步,如果我们现在只能使用Context包,可以改下成如下代码:

func coordinateWithContext() {
	total := 12
	var num int32
	fmt.Printf("num = %d [with context.Context]\n", num)
	cxt, cancelFunc := context.WithCancel(context.Background())
	for i := 1; i <= total; i++ {
		go addNum(&num, i, func() {
			if atomic.LoadInt32(&num) == int32(total) {
				cancelFunc()
			}
		})
	}
	<-cxt.Done()
	fmt.Println("End.")
}

通过context.WithCancel函数包装context.Background函数获得的对象最终得到了一个可撤销的context.Context类型的值,以及一个context.CancelFunc类型的撤销函数。

cxt变量的Done函数会返回一个通道,这里调用了接收操作,会等待cancelFunc函数被调用。

上述代码意味着当所有addNum函数都执行完毕时,cancelFunc才会被调用,整个coordinateWithContext函数才会结束。

Context的接口定义的比较简洁:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}

Deadline方法是获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。

Done方法返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求。

Err方法返回Context被取消的错误原因,。

Value方法获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。


Context值可以产生出任意个子值,这些子值可以携带其父值的属性和数据,也可以响应我们通过其父值传达的信号。所有的Context值共同构成了一颗代表了上下文全貌的树形结构。Go内置的Background实现类是全局唯一的,一般作为这棵树的树根。

不过Background不提供任何额外的功能,既不可以被撤销(cancel),也不能携带任何数据。

衍生更多的子Context靠context包提供的With函数:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。

WithCancel函数:返回子Context和一个撤销函数。

WithDeadline函数:需多传递一个截止时间,超时会自动取消Context。当然也可以通过撤销函数提前进行撤销。WithTimeout功能类似,区别是传入超时时长。

WithValue函数:生成绑定了键值对数据的Context,可通过Context.Value获取。

在撤销函数被调用之后,对应的Context值会先关闭它内部的接收通道,也就是它的Done方法会返回的那个通道。然后向所有的子值传达撤销信号,子值也会向自己的子值传递撤销信号,最终每个子值都会断开自己与父值之间的关联。大致是如下形式:

image-20211220183224646

调用context.WithValue函数得到的Context值是不可撤销的。撤销信号在被传播时,若遇到它们则会直接跨过,并试图将信号直接传给它们的子值。

Context类型的Value方法会先判断给定的键,是否与当前值中存储的键相等,如果相等就把该值中存储的值直接返回,否则就到其父值中继续查找。除了含数据的Context值以外,其他几种Context值都是无法携带数据的。因此查找时会直接跳过那些可撤销的Context

context.WithValue的示例:

package main

import (
	"context"
	"fmt"
	"time"
)

var key string = "name"

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	//附加值
	valueCtx := context.WithValue(ctx, key, "【监控1】")
	go watch(valueCtx, cancel)
	<-ctx.Done()
	fmt.Println("监控停止")
}

func watch(ctx context.Context, cancel func()) {
	for i := 1; i <= 5; i++ {
		fmt.Println("ctx.Value(key)=", ctx.Value(key))
		time.Sleep(200 * time.Millisecond)
	}
	cancel()
}

输出:

ctx.Value(key)= 【监控1】
ctx.Value(key)= 【监控1】
ctx.Value(key)= 【监控1】
ctx.Value(key)= 【监控1】
ctx.Value(key)= 【监控1】
监控停止

Go语言的标准库

标准库概述

下面按功能进行分组介绍内置包的简单用途:

  • unsafe: 包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中。例如unsafe.Sizeof(i)获取变量i占用的字节。

  • syscall-os-os/exec:

    • os: 提供操作系统功能接口,采用类Unix设计,隐藏了不同操作系统间差异,让不同的文件系统和操作系统对象表现一致。
    • os/exec: 提供我们运行外部操作系统命令和程序的方式。
    • syscall: 底层的外部包,提供了操作系统底层调用的基本接口。
  • archive/tar/zip-compress:压缩(解压缩)文件功能。

  • fmt-io-bufio-path/filepath-flag:

    • fmt: 提供了格式化输入输出功能。
    • io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
    • bufio: 缓冲输入输出功能的封装。
    • path/filepath: 用来操作在当前系统中的目标文件名路径。
    • flag: 对命令行参数的操作。
  • strings-strconv-unicode-regexp-bytes:

    • strings: 提供对字符串的操作。
    • strconv: 提供将字符串转换为基础类型的功能。
    • unicode: 为 unicode 型的字符串提供特殊的功能。
    • regexp: 正则表达式功能。
    • bytes: 提供对字符型分片的操作。
    • index/suffixarray: 子字符串快速查询。
  • math-math/cmath-math/big-math/rand-sort:

    • math: 基本的数学函数。
    • math/cmath: 对复数的操作。
    • math/rand: 伪随机数生成。
    • sort: 为数组排序和自定义集合。
    • math/big: 大数的实现和计算。
  • container-/list-ring-heap: 实现对集合的操作。

    • list: 双链表。

    • 示例:使用 container/list 包将 101、102 和 103 放入其中并打印出来。

      package main
      
      import (
      	"container/list"
      	"fmt"
      )
      
      func main() {
      	lst := list.New()
      	lst.PushBack(100)
      	lst.PushBack(101)
      	lst.PushBack(102)
      	for e := lst.Front(); e != nil; e = e.Next() {
      		fmt.Println(e.Value)
      	}
      }
      
    • ring: 环形链表。

  • time-log:

    • time: 日期和时间的基本操作。
    • log: 记录程序运行时产生的日志。
  • encoding/json-encoding/xml-text/template:

    • encoding/json: 读取并解码和写入并编码 JSON 数据。
    • encoding/xml: 简单的 XML1.0 解析器。
    • text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板。
  • net-net/http-html:

    • net: 网络数据的基本操作。
    • http: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。
    • html: HTML5 解析器。
  • runtime: Go 程序运行时的交互操作,例如垃圾回收和协程创建。

  • reflect: 实现通过程序运行时反射,让程序操作任意类型的变量。

随机数rand

package main
import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    for i := 0; i < 3; i++ {
        a := rand.Int()
        fmt.Printf("%d / ", a)
    }
    fmt.Println()
    for i := 0; i < 5; i++ {
        r := rand.Intn(8)
        fmt.Printf("%d / ", r)
    }
    fmt.Println()
    timens := int64(time.Now().Nanosecond())
    rand.Seed(timens)
    for i := 0; i < 5; i++ {
        fmt.Printf("%2.2f / ", 100*rand.Float32())
    }
}

结果:

5577006791947779410 / 8674665223082153551 / 6129484611666145821 /
3 / 1 / 6 / 1 / 4 /
98.94 / 30.96 / 28.01 / 0.88 / 61.93 /

函数 rand.Float32rand.Float64 返回介于 [0.0, 1.0) 之间的伪随机数,其中包括 0.0 但不包括 1.0。函数 rand.Intn返回介于 [0, n) 之间的伪随机数。

可以使用 Seed(value) 函数来提供伪随机数的生成种子,默认一般使用当前时间的纳秒级数字。

net/http库

示例:

package main

import "net/http"

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello, world"))
	})
	http.ListenAndServe(":8080", nil)
}

http.HandleFunc的第一个字符串匹配URL路径,采用最长前缀匹配的规则,"/"意味着能够匹配所有的URI 路径。

运行后,使用游览器访问可以看到返回的hello, world字符串。

字符串工具类strings

前缀和后缀:

HasPrefix 判断字符串 s 是否以 prefix 开头:

strings.HasPrefix(s, prefix string) bool

HasSuffix 判断字符串 s 是否以 suffix 结尾:

strings.HasSuffix(s, suffix string) bool

字符串包含关系:

Contains 判断字符串 s 是否包含 substr

strings.Contains(s, substr string) bool

获取索引位置:

Index 返回字符串 str 在字符串 s 中的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str

strings.Index(s, str string) int

LastIndex 返回字符串 str 在字符串 s 中最后出现位置的索引(str 的第一个字符的索引),-1 表示字符串 s 不包含字符串 str

strings.LastIndex(s, str string) int

字符串替换:

Replace 用于将字符串 str 中的前 n 个字符串 old 替换为字符串 new,并返回一个新的字符串,如果 n = -1 则替换所有字符串 old 为字符串 new

strings.Replace(str, old, new, n) string

统计出现次数:

Count 用于计算字符串 str 在字符串 s 中出现的非重叠次数:

strings.Count(s, str string) int

重复字符串:

Repeat 用于重复 count 次字符串 s 并返回一个新的字符串:

strings.Repeat(s, count int) string

大小写:

ToLower 将字符串中的 Unicode 字符全部转换为相应的小写字符:

strings.ToLower(s) string

ToUpper 将字符串中的 Unicode 字符全部转换为相应的大写字符:

strings.ToUpper(s) string

trim:

删除首尾空白字符:

 strings.TrimSpace(s) string

删除首尾指定字符(第二个参数表示要删除的字符列表,包含任务一个字符都会被删除),例如:

strings.Trim(s, "cut")

只删首或只删尾使用TrimLeftTrimRight 实现。

分割字符串:

strings.Split(s, sep) 用于指定分割符切割字符串返回 slice。

strings.Fields(s) 则指定空白字符切割字符串。

拼接 slice 到字符串:

strings.Join(sl []string, sep string) string

例如:

str := []string{"Google","Baidu","Taobao","Weibo"}
strings.Join(str,"|")

更多字符串操作可参考:http://docs.studygolang.com/pkg/strings/

从字符串中读取内容:

函数 strings.NewReader(str) 用于生成一个 Reader 并读取字符串中的内容,然后返回指向该 Reader 的指针,从其它类型读取内容的函数还有:

  • Read() 从 []byte 中读取内容。
  • ReadByte()ReadRune() 从字符串中读取下一个 byte 或者 rune。
package main

import (
    "fmt"
    "strings"
)

func main() {
    buf := make([]byte, 3)
    in := strings.NewReader("aaa bbb ccc dddee")
    for {
        len,_:=in.Read(buf)
        if len==0 {
            break
        }
        fmt.Println(string(buf[:len]))
    }
}

结果:

aaa
 bb
b c
cc
ddd
ee

更多用法:http://docs.studygolang.com/pkg/strings/

strings.Builder 与 bytes.Buffer

Builder值中有一个用于承载内容的容器,它是一个以byte为元素类型的字节切片。

Builder与string字符串一样都是通过一个unsafe.Pointer类型的字段来持有那个指向了底层字节数组的指针值的。

虽然字节切片本身来的任何元素值都可以被修改,但是Builder值的内容只能够被拼接或者完全重置。已存在于Builder值中的内容是不可变的。

Builder值拥有WriteWriteByteWriteRuneWriteString等拼接方法,自动扩容策略与切片的扩容策略一致。调用Builder值的Grow方法可以手动扩容。而Reset方法可以让Builder值重新回到零值状态,就像它从未被使用过那样。

示例:

	var builder1 strings.Builder
	builder1.WriteString("WriteString方法可以追加写入一些字符串.")
	fmt.Println("当前状态:", builder1.Len(), builder1.String())
	builder1.WriteByte(' ')
	builder1.WriteString("相对字符串拼接减少了内存复制")
	builder1.Write([]byte{'\n', '\n'})
	builder1.WriteString("hello")
	fmt.Println("当前状态:", builder1.Len(), builder1.String())

	builder1.Grow(10)
	fmt.Println("调用builder1.Grow(10)后:", builder1.Len())
	fmt.Println()

	builder1.Reset()
	fmt.Println("Reset重置后:", builder1.Len(), builder1.String())

输出:

当前状态: 51 WriteString方法可以追加写入一些字符串.
当前状态: 101 WriteString方法可以追加写入一些字符串. 相对字符串拼接减少了内存复制

hello
调用builder1.Grow(10)后: 101

Reset重置后: 0 

如有必要,Grow方法会原容器容量的二倍再加上n,而未用容量大于或等于n时却什么都不做。

strings.Builder类型在使用上的约束

strings.Builder类型在真正使用后就不可再被复制,例如以下代码注释的这行会报错:

var builder1 strings.Builder
builder1.Grow(1)
builder3 := builder1
//builder3.Grow(1) // 这里会引发 panic。

strings.Builder的指针类型实现的接口有io.Writerio.ByteWriterfmt.Stringer,以及包级私有接口io.StringWriter

bytes.Buffer类型

bytes.Buffer用作字节序列的缓冲区,strings.Builder只能拼接和导出字符串,而bytes.Buffer不但可以拼接、截断其中的字节序列,以各种形式导出其中的内容,还可以顺序地读取其中的子序列。

package main

import (
	"bytes"
	"fmt"
)

func main() {
	var buffer1 bytes.Buffer
	contents := "用于封送数据的简单字节缓冲区。 "
	buffer1.WriteString(contents)
	fmt.Printf("len=%d,cap=%d\n", buffer1.Len(), buffer1.Cap())

	p1 := make([]byte, 7)
	n, _ := buffer1.Read(p1)
	fmt.Printf("读取字节数:%d,len=%d,cap=%d,content=%v\n", n, buffer1.Len(), buffer1.Cap(), p1)
	n, _ = buffer1.Read(p1)
	fmt.Printf("读取字节数:%d,len=%d,cap=%d,content=%v\n", n, buffer1.Len(), buffer1.Cap(), p1)
}

输出:

len=46,cap=64
读取字节数:7,len=39,cap=64,content=[231 148 168 228 186 142 229]
读取字节数:7,len=32,cap=64,content=[176 129 233 128 129 230 149]

其中n代表已读计数,一开始向缓冲区写入了长度为46的字符串,容量64符合自动扩容策略。两次读取7个字节的内容后,缓冲区的长度也会减少相应的长度。

bytes.Buffer 的指针类型实现的读取相关的接口有下面几个:

  1. io.Reader
  2. io.ByteReader
  3. io.RuneReader
  4. io.ByteScanner
  5. io.RuneScanner
  6. io.WriterTo

共有 6 个。而其实现的写入相关的接口则有这些。

  1. io.Writer
  2. io.ByteWriter
  3. io.stringWriter
  4. io.ReaderFrom

strconv字符串的类型转换

与字符串相关的类型转换都是通过 strconv 包实现的。

当前操作系统下int 类型所占的位数:

strconv.IntSize

返回数字 i 所表示的字符串类型的十进制数:

strconv.Itoa(i int) string

将浮点型的数字转换为字符串:

strconv.FormatFloat(f float64, fmt byte, prec int, bitSize int)

fmt 表示格式(其值可以是 'b''e''f''g'),prec 表示精度,bitSize 则使用 32 表示 float32,用 64 表示 float64。

将字符串转换为 int 型:

strconv.Atoi(s string) (i int, err error)

将字符串转换为 float64 型:

strconv.ParseFloat(s string, bitSize int) (f float64, err error)

分别返回转换成功的结果和可能出现的错误。

利用多返回值的特性,这些函数会返回 2 个值,第 1 个是转换后的结果(如果转换成功),第 2 个是可能出现的错误,因此,我们一般使用以下形式来进行从字符串到其它类型的转换:

val, err = strconv.Atoi(s)

示例:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	fmt.Println("当前操作系统下int 类型所占的位数:", strconv.IntSize)

	num, _ := strconv.Atoi("777")
	fmt.Printf("%d %T\n", num, num)
	num += 5
	newS := strconv.Itoa(num)
	fmt.Printf("%s %T\n", newS, newS)
}

输出:

当前操作系统下int 类型所占的位数: 64
777 int
782 string

更多strconv包的用法参考:http://docs.studygolang.com/pkg/strconv/

container包中的容器

在标准库的container/list代码包中有双向链表List ,链表中元素的结构为Element 。

List的四种主要方法:

func (l *List) MoveBefore(e, mark *Element)
func (l *List) MoveAfter(e, mark *Element)
 
func (l *List) MoveToFront(e *Element)
func (l *List) MoveToBack(e *Element)

MoveBefore方法和MoveAfter方法,它们分别用于把给定的元素移动到另一个元素的前面和后面。

MoveToFront方法和MoveToBack方法,分别用于把给定的元素移动到链表的最前端和最后端。

List的方法还有下面这几种:

func (l *List) Front() *Element
func (l *List) Back() *Element
 
func (l *List) InsertBefore(v interface{}, mark *Element) *Element
func (l *List) InsertAfter(v interface{}, mark *Element) *Element
 
func (l *List) PushFront(v interface{}) *Element
func (l *List) PushBack(v interface{}) *Element

FrontBack方法分别用于获取链表中最前端和最后端的元素,
InsertBeforeInsertAfter方法分别用于在指定的元素之前和之后插入新元素,PushFrontPushBack方法则分别用于在链表的最前端和最后端插入新元素。

这些方法都会把一个Element值的指针作为结果返回,拿到这些内部元素的指针,就可以去调用前面移动元素的方法。

示例:

package main

import (
    "container/list"
    "fmt"
)

func main() {
    var l list.List
    // l := list.New()
    e4 := l.PushBack(4)
    e1 := l.PushFront(1)
    l.InsertBefore(3, e4)
    l.InsertAfter(2, e1)

    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }
}

结果:

1
2
3
4

ListElement都是结构体类型。结构体类型的零值都会是拥有特定结构,但是没有任何定制化内容的值。值中的字段也都会被分别赋予各自类型的零值。

语句var l list.List声明的链表l可以直接使用,关键在于它的“延迟初始化”机制。链表的PushFront方法、PushBack方法、PushBackList方法以及PushFrontList方法总会先判断链表的状态,并在必要时进行初始化。在向一个空的链表中添加新元素的时候,肯定会调用这四个方法中的一个,这时新元素中指向所属链表的指针,一定会被设定为当前链表的指针。

更多用法参考:http://docs.studygolang.com/pkg/container/list/

RingList的区别 最主要的不同有下面几种:

  1. Ring类型的数据结构仅由它自身即可代表,而List类型则需要由它以及Element类型联合表示。
  2. 一个Ring类型的值严格来讲,只代表了其所属的循环链表中的一个元素,而一个List类型的值则代表了一个完整的链表。
  3. 在创建并初始化一个Ring时,其长度不可变,可以指定它包含的元素的数量。
  4. var r ring.Ring语句声明的r将会是一个长度为1的循环链表,而List类型的零值则是一个长度为0的链表。
  5. Ring值的Len方法的算法复杂度是 O(N) ,而List值的Len方法的算法复杂度则是 O(1) 的。

示例:

package main

import (
    "container/ring"
    "fmt"
)

func main() {
    r := ring.New(5)
    n := r.Len()
    for i := 0; i < n; i++ {
        r.Value = i
        r = r.Next()
    }
    r.Do(func(p interface{}) {
        fmt.Println(p.(int))
    })
}

结果:

0
1
2
3
4

更多用法参考:http://docs.studygolang.com/pkg/container/ring/

container包下还有一个数据结构堆:http://docs.studygolang.com/pkg/container/heap/

并发安全字典sync.Map

在Go 1.9 之前,要实现并发安全的字典,就只能自行构建。从1.9 之后,官方正式加入了并发安全的字典类型sync.Map。与单纯使用原生map和互斥锁的方案相比,使用sync.Map可以显著地减少锁的争用。sync.Map本身虽然也用到了锁,但是,它其实在尽可能地避免使用锁。

Go 语言的原生字典的键类型不能是函数类型、字典类型和切片类型。并发安全字典内部使用的存储介质是原生字典,键类型也是interface{};所以不能使用函数类型、字典类型或切片类型的键值去操作并发安全字典。

这些键值的实际类型只有在程序运行期间才能够确定,Go 语言编译器是无法在编译期对它们进行检查的,不正确的键值实际类型肯定会引发 panic。

使用示例:

package main

import (
	"fmt"
	"sync"
)

func main() {
	var sMap sync.Map
	// 存储键值对
	sMap.Store(1, "a")
	sMap.Store(2, "b")
	sMap.Store(3, "c")
	sMap.Store(4, "d")
	// 遍历
	sMap.Range(func(key, value interface{}) bool {
		fmt.Printf("key=%v, value=%v\n", key, value)
		return true
	})
	// 读取指定键
	v0, ok := sMap.Load(1)
	fmt.Printf("sMap.Load(1): v=%v, ok=%v\n", v0, ok)

	// 指定键不存在事则先存储后再返回
	actual2, loaded2 := sMap.LoadOrStore(3, "c")
	fmt.Printf("sMap.LoadOrStore(3, 'c'): actual2=%v, loaded2=%v\n", actual2, loaded2)

	// 删除指定键
	sMap.Delete(2)
	fmt.Println("删除key=2之后")
	v2, ok := sMap.Load(2)
	fmt.Printf("sMap.Load(2): v=%v, ok=%v\n", v2, ok)

	actual1, loaded1 := sMap.LoadOrStore(2, "e")
	fmt.Printf("sMap.LoadOrStore(2, 'a'): actual1=%v, loaded1=%v\n", actual1, loaded1)

	sMap.Range(func(key, value interface{}) bool {
		fmt.Printf("key=%v, value=%v\n", key, value)
		return true
	})
}

结果:

key=1, value=a
key=2, value=b
key=3, value=c
key=4, value=d
sMap.Load(1): v=a, ok=true
sMap.LoadOrStore(3, 'c'): actual2=c, loaded2=true
删除key=2之后
sMap.Load(2): v=<nil>, ok=false
sMap.LoadOrStore(2, 'a'): actual1=e, loaded1=false
key=2, value=e
key=3, value=c
key=4, value=d
key=1, value=a

如果我们自己实现一个并发安全字典,代码如下:

// ConcurrentMap 代表自制的简易并发安全字典。
type ConcurrentMap struct {
	m  map[interface{}]interface{}
	mu sync.RWMutex
}

func NewConcurrentMap() *ConcurrentMap {
	return &ConcurrentMap{
		m: make(map[interface{}]interface{}),
	}
}

func (cMap *ConcurrentMap) Delete(key interface{}) {
	cMap.mu.Lock()
	defer cMap.mu.Unlock()
	delete(cMap.m, key)
}

func (cMap *ConcurrentMap) Load(key interface{}) (value interface{}, ok bool) {
	cMap.mu.RLock()
	defer cMap.mu.RUnlock()
	value, ok = cMap.m[key]
	return
}

func (cMap *ConcurrentMap) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool) {
	cMap.mu.Lock()
	defer cMap.mu.Unlock()
	actual, loaded = cMap.m[key]
	if loaded {
		return
	}
	cMap.m[key] = value
	actual = value
	return
}

func (cMap *ConcurrentMap) Range(f func(key, value interface{}) bool) {
	cMap.mu.RLock()
	defer cMap.mu.RUnlock()
	for k, v := range cMap.m {
		if !f(k, v) {
			break
		}
	}
}

func (cMap *ConcurrentMap) Store(key, value interface{}) {
	cMap.mu.Lock()
	defer cMap.mu.Unlock()
	cMap.m[key] = value
}

regexp 包

使用 MatchMatchString方法进行匹配:

ok, _ := regexp.Match(pat, []byte(searchIn))
ok, _ := regexp.MatchString(pat, searchIn)

变量 ok 将返回 true 或者 false,表示是否匹配成功。

更多方法中,必须先将正则通过 Compile 方法返回一个 Regexp 对象。

示例:

package main

import (
	"fmt"
	"regexp"
	"strconv"
)

func main() {
	if ok, _ := regexp.MatchString("com", "www.taobao.com"); ok {
		fmt.Println("匹配成功!")
	}

	re, _ := regexp.Compile("#.*$")
	// 删除注释
	str := re.ReplaceAllString("2004-959-559 # 这是一个电话号码", "")
	fmt.Println(str)

	s := "A23G4HFD567"
	// 将字符串中的匹配的数字乘于 2:
	f := func(s string) string {
		v, _ := strconv.Atoi(s)
		return strconv.Itoa(v * 2)
	}
	re, _ = regexp.Compile("\\d+")
	str2 := re.ReplaceAllStringFunc(s, f)
	fmt.Println(str2)
}

结果:

匹配成功!
2004-959-559
A46G8HFD1134

大数运算big 包

在大部分语言中, float类型的浮点数都存在一定的精度问题。在对精度有严格要求时,我可以使用Go 语言提供的math/big包,表示大整数的 big.Int 和表示大有理数的big.Rat 类型(不支持无理数)。

大的整型数字是通过 big.NewInt(n) 来构造的,其中 n 位 int64 类型整数。而大有理数是用过 big.NewRat(N,D) 方法构造。N(分子)和 D(分母)都是 int64 型整数。

package main

import (
    "fmt"
    "math"
    "math/big"
)

func main() {
    // Here are some calculations with bigInts:
    im := big.NewInt(math.MaxInt64)
    in := im
    io := big.NewInt(1956)
    ip := big.NewInt(1)
    ip.Mul(im, in).Add(ip, im).Div(ip, io)
    fmt.Printf("Big Int: %v\n", ip)
    // Here are some calculations with bigInts:
    rm := big.NewRat(math.MaxInt64, 1956)
    rn := big.NewRat(-1956, math.MaxInt64)
    ro := big.NewRat(19, 56)
    rp := big.NewRat(1111, 2222)
    rq := big.NewRat(1, 1)
    rq.Mul(rm, rn).Add(rq, ro).Mul(rq, rp)
    fmt.Printf("Big Rat: %v\n", rq)
}

time时间日期

time.Time提供了显示和测量时间和日期的功能函数。

获取当前时间:

time.Now()

t.Day()、t.Month()、t.Year()可获取时间的一部分。示例:

package main

import (
	"fmt"
	"time"
)

func main() {
	t := time.Now()
	fmt.Println(t.Year(), t.Month(), t.Day())
	fmt.Printf("%04d-%02d-%02d", t.Year(), t.Month(), t.Day())
}

结果:

2021 December 3
2021-12-03

Duration 类型表示两个连续时刻所相差的纳秒数,类型为 int64。Location 类型映射某个时区的时间,UTC 表示通用协调世界时间。

Format函数可以根据一个格式化字符串来将一个时间 t 转换为相应格式的字符串。

预定义格式有:

const (
    Layout      = "01/02 03:04:05PM '06 -0700" // The reference time, in numerical order.
    ANSIC       = "Mon Jan _2 15:04:05 2006"
    UnixDate    = "Mon Jan _2 15:04:05 MST 2006"
    RubyDate    = "Mon Jan 02 15:04:05 -0700 2006"
    RFC822      = "02 Jan 06 15:04 MST"
    RFC822Z     = "02 Jan 06 15:04 -0700" // RFC822 with numeric zone
    RFC850      = "Monday, 02-Jan-06 15:04:05 MST"
    RFC1123     = "Mon, 02 Jan 2006 15:04:05 MST"
    RFC1123Z    = "Mon, 02 Jan 2006 15:04:05 -0700" // RFC1123 with numeric zone
    RFC3339     = "2006-01-02T15:04:05Z07:00"
    RFC3339Nano = "2006-01-02T15:04:05.999999999Z07:00"
    Kitchen     = "3:04PM"
    // Handy time stamps.
    Stamp      = "Jan _2 15:04:05"
    StampMilli = "Jan _2 15:04:05.000"
    StampMicro = "Jan _2 15:04:05.000000"
    StampNano  = "Jan _2 15:04:05.000000000"
)

示例:

package main

import (
	"fmt"
	"time"
)

var week time.Duration

func main() {
	t := time.Now()
	fmt.Println("t:", t)
	fmt.Println("t.UTC:", t.UTC())

	week = 60 * 60 * 24 * 7 * 1e9
	week_from_now := t.Add(week)
	fmt.Println("七天后:", week_from_now)

	fmt.Println("预定义格式 RFC3339:", t.Format(time.RFC3339))
	fmt.Println(t.Format("2006-01-02 15:04:05"))
	fmt.Println(week_from_now.Format("2006-01-02 15:04:05"))
}

结果:

t: 2021-12-03 17:26:14.5075289 +0800 CST m=+0.005766001
t.UTC: 2021-12-03 09:26:14.5075289 +0000 UTC
七天后: 2021-12-10 17:26:14.5075289 +0800 CST m=+604800.005766001
预定义格式 RFC3339: 2021-12-03T17:26:14+08:00
2021-12-03 17:26:14
2021-12-10 17:26:14

演示一下Ticker定时器的用法:

package main

import (
    "fmt"
    "time"
)

func main() {
    ticker := time.NewTicker(time.Second)
    defer ticker.Stop()
    done := make(chan bool)
    go func() {
        time.Sleep(4 * time.Second)
        done <- true
    }()
    for {
        select {
        case <-done:
            fmt.Println("Done!")
            return
        case t := <-ticker.C:
            fmt.Println("当前时间: ", t.Format("15:04:05"))
        }
    }
}

结果:

当前时间:  19:07:57
当前时间:  19:07:58
当前时间:  19:07:59
当前时间:  19:08:00
Done!

更多用法:http://docs.studygolang.com/pkg/time/

读写数据

标准输入与输出

从键盘和标准输入 os.Stdin 读取输入,最简单的办法是使用 fmt 包提供的 Scan 和 Sscan 开头的函数。

示例:

package main

import "fmt"

func main() {
	var nickName, sex string
	fmt.Println("请输入昵称和性别: ")
	// fmt.Scanln(&nickName, &sex)
	fmt.Scanf("%s %s", &nickName, &sex)
	fmt.Printf("昵称:%s,性别:%s!\n", nickName, sex)

	var (
		s      string
		i      int
		f      float32
		input  = "56.12 / 5212 / Go"
		format = "%f / %d / %s"
	)
	fmt.Sscan("小华 男", &nickName, &sex)
	fmt.Println(nickName, sex)
	fmt.Sscanf(input, format, &f, &i, &s)
	fmt.Printf("读取结果:%f / %d / %s", f, i, s)
}

输出:

请输入昵称和性别:
小小明 男
昵称:小小明,性别:男!
小华 男
读取结果:56.119999 / 5212 / Go

Scanln 扫描标准输入,按空格分隔放入每个参数内,直到碰到换行。Scanf 的第一个参数则定义了读取格式。Sscan 开头的函数则是从字符串读取。

bufio 包提供了缓冲读取(buffered reader)的功能:

package main

import (
	"bufio"
	"fmt"
	"os"
	"strings"
)

var input string

func main() {
	var inputReader *bufio.Reader = bufio.NewReader(os.Stdin)
	for {
		fmt.Print("请输入: ")
		input, _ = inputReader.ReadString('\n')
		input = strings.TrimSpace(input)
		if input == "quit" {
			break
		}
		fmt.Println("反馈:", input)
	}
}

如果使用ReadString 返回读取到的字符串,如果碰到错误则返回 nil。如果它一直读到文件结束,则返回读取到的字符串和io.EOF。如果读取过程中没有碰到 delim 字符,将返回错误 err != nil

标准输出 os.Stdoutos.Stderr 用于显示错误信息。

文件读取

在 Go 语言中,文件使用 os.File 类型的指针来表示的,也叫做文件句柄。标准输入os.Stdin 和标准输出 os.Stdout的类型都是 *os.File

按行读取文件示例:

创建 tmp.txt 文件内容为:

这是第一行
这是第二行
这是第三行
这是第四行
这是第五行

代码:

package main

import (
	"bufio"
	"fmt"
	"io"
	"os"
	"strings"
)

func main() {
	inputFile, inputError := os.Open("tmp.txt")
	if inputError != nil {
		fmt.Printf("打开文件发生错误:", inputError)
		return
	}
	defer inputFile.Close()

	inputReader := bufio.NewReader(inputFile)
	for {
		line, err := inputReader.ReadString('\n')
		line = strings.TrimSpace(line)
		fmt.Println(line)
		if err == io.EOF {
			return
		}
	}
}

结果顺利的打印出文件中的内容。

上述代码使用 ReadString('\n') 将文件的内容逐行读取,读取时会包含分隔符。 ReadBytes('\n') 也可以按行读取,不过读取结果是字节数组类型([]uint8)。

我们也可以使用 ReadLine()方法来实现相同的功能:

package main

import (
	"bufio"
	"fmt"
	"os"
	"io"
)

func main() {
	inputFile, inputError := os.Open("tmp.txt")
	if inputError != nil {
		fmt.Printf("打开文件发生错误:", inputError)
		return
	}
	defer inputFile.Close()

	inputReader := bufio.NewReader(inputFile)
	for i := 0; i < 7; i++ {
		bytes, _, err := inputReader.ReadLine()
		if err == io.EOF {
			return
		}
		line := string(bytes)
		fmt.Println(line)
	}
}

按列读取文件示例:

package main
import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("products2.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    var col1, col2, col3 []string
    for {
        var v1, v2, v3 string
        _, err := fmt.Fscanln(file, &v1, &v2, &v3)
        // scans until newline
        if err != nil {
            break
        }
        col1 = append(col1, v1)
        col2 = append(col2, v2)
        col3 = append(col3, v3)
    }

    fmt.Println(col1)
    fmt.Println(col2)
    fmt.Println(col3)
}

注意:path 包里包含一个子包叫 filepath,这个子包提供了跨平台的函数,用于处理文件名和路径。例如 Base() 函数用于获得路径中的最后一个元素(不包含后面的分隔符):

import "path/filepath"
filename := filepath.Base(path)

compress包提供了读取压缩文件的功能,支持的压缩文件格式为:bzip2、flate、gzip、lzw 和 zlib。

下面的程序展示了如何读取一个 gzip 文件:

package main

import (
    "fmt"
    "bufio"
    "os"
    "compress/gzip"
    "strings"
)

func main() {
    fName := "tips.csv.gz"
    var r *bufio.Reader
    fi, err := os.Open(fName)
    if err != nil {
        panic(err)
    }
    fz, err := gzip.NewReader(fi)
    if err != nil {
        r = bufio.NewReader(fi)
    } else {
        r = bufio.NewReader(fz)
    }
	
    for {
        line, err := r.ReadString('\n')
        if err != nil {
            fmt.Println("读取完毕")
            break
        }
        line = strings.TrimSpace(line)
        fmt.Println(line)
    }
}

文件写入

示例:

package main

import (
    "os"
    "bufio"
    "fmt"
)

func main () {
    outputFile, err := os.OpenFile("output.dat", os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Println(err)
        return  
    }
    defer outputFile.Close()

    outputWriter := bufio.NewWriter(outputFile)
    for i:=1; i<10; i++ {
            outputWriter.WriteString(fmt.Sprintf("第%d行\n",i))
    }
    outputWriter.Flush()
}

OpenFile 函数有三个参数:文件名、一个或多个标志(使用逻辑运算符“|”连接),使用的文件权限。

我们通常会用到以下标志:

  • os.O_RDONLY:只读
  • os.WRONLY:只写
  • os.O_CREATE:创建:如果指定文件不存在,就创建该文件。
  • os.O_TRUNC:截断:如果指定文件已存在,就将该文件的长度截为0。

在读文件的时候,文件的权限是被忽略的,所以在使用 OpenFile 时传入的第三个参数可以用0。而在写文件时,不管是 Unix 还是 Windows,都需要使用 0666。

如果写入的东西很简单,我们可以使用 fmt.Fprintf(outputFile, “Some test data.\n”) 直接将内容写入文件。fmt 包里的 F 开头的 Print 函数可以直接写入任何 io.Writerf.WriteString( )不使用缓冲区,直接将内容写入文件。

实现文件拷贝:

package main

import (
	"fmt"
	"io"
	"os"
)

func main() {
	_, err := CopyFile("1.txt", "1_copy.txt")
	if err == nil {
		fmt.Println("复制完成")
	}

}

func CopyFile(srcName, dstName string) (written int64, err error) {
	src, err := os.Open(srcName)
	if err != nil {
		panic(err)
	}
	defer src.Close()

	dst, err := os.OpenFile(dstName, os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		panic(err)
	}
	defer dst.Close()

	return io.Copy(dst, src)
}

文件拷贝还可以使用 io/ioutil 包,其中的 ioutil.ReadFile() 方法,可以将整个文件读取到变量中。函数WriteFile() 可以将 []byte 的值写入文件。

例如:

package main

import (
	"fmt"
	"io/ioutil"
)

func main() {
	err := CopyFile("1.txt", "1_copy.txt")
	if err == nil {
		fmt.Println("复制完成")
	}
}

func CopyFile(in, out string) (err error) {
	buf, err := ioutil.ReadFile(in)
	if err != nil {
		return err
	}
	err = ioutil.WriteFile(out, buf, 0x644)
	return err
}

从命令行读取参数

os包中有一个string类型的切片变量os.Args,可以获取命令行参数:

package main

import (
    "fmt"
    "os"
    "strings"
)

func main() {
    text:=strings.Join(os.Args[1:], " ")
    fmt.Println(text)
}

使用以下命令执行:

go run demo.go one two three

输出:

one two three

命令行参数会放置在切片os.Args[]从索引1开始,os.Args[0]放的是程序本身的名字

flag包有一个扩展功能用来解析命令行选项。flag下的Flag结构体包含如下字段:

type Flag struct {
    Name     string  // 名称
    Usage    string  // 帮助信息
    Value    Value   // 值
    DefValue string  // 默认值
}

示例:

package main

import (
	"flag" // command line option parser
	"fmt"
)

var user = flag.String("u", "user1", "user")
var color = flag.String("c", "blue", "color")
var speed = flag.Bool("n", false, "bool")

func main() {
	flag.PrintDefaults()
	fmt.Println("----------------------")
	flag.Parse()
	// 访问名称参数
	fmt.Println(*user, *color, *speed)
	// 访问位置参数
	for i := 0; i < flag.NArg(); i++ {
		fmt.Println(flag.Arg(i))
	}
	// 访问所有名称参数
	flag.VisitAll(func(x *flag.Flag) { fmt.Println(x.Value) })
}

输出:

>go run demo.go -u xxmdmst -c red -n one two three
  -c string
        color (default "blue")
  -n    bool
  -u string
        user (default "user1")
----------------------
xxmdmst red true
one
two
three
red
true
xxmdmst

上述示例使用了flag.String,它会直接返回一个已经分配好的用于存储命令参数值的地址,还有一个类似的函数flag.StringVar,例如:

flag.StringVar(&name, "name", "everyone", "The greeting object.")

函数flag.StringVar接受 4 个参数:

  1. 用于存储该命令参数值的地址,具体到这里就是在前面声明的变量name的地址了,由表达式&name表示。
  2. 指定该命令参数的名称,这里是name
  3. 指定在未追加该命令参数时的默认值,这里是everyone
  4. 该命令参数的简短说明在打印命令说明时会用到。

例如:

package main

import (
	"flag"
	"fmt"
)

var name string

func init() {
	flag.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
	flag.Parse()
	fmt.Printf("Hello, %s!\n", name)
}

调用示例:

>go run demo2.go -name "aaa"
Hello, aaa!

>go run demo2.go -name aaa
Hello, aaa!

>go run demo2.go -name=aaa
Hello, aaa!

>go run demo2.go -name="aaa"
Hello, aaa!

查看参数使用说明:

go run demo2.go --help

如果需要自定义命令源码文件的参数使用说明,可以对变量flag.Usage重新赋值。在main函数体的开始处加入如下代码:

flag.Usage = func() {
	fmt.Fprintf(os.Stderr, "Usage of %s:\n", "param")
	flag.PrintDefaults()
}

深入一点,我们在调用flag包中的一些函数(比如StringVarParse等等)的时候,实际上是在调用flag.CommandLine变量的对应方法,默认情况下它相当于命令参数容器。

这样我们还可以通过flag.CommandLine自定义参数使用说明,在init函数体的开始处添加如下代码:

flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
	fmt.Fprintf(os.Stderr, "Usage of %s:\n", "param")
	flag.PrintDefaults()
}

flag.NewFlagSet函数的第二个参数值当命令后跟--help或者参数设置的不正确的时的处理方式,参数值为flag.ExitOnError时将以状态码2结束当前程序,状态码2代表用户错误地使用了命令。而参数值为flag.PanicOnError时会抛出“运行时恐慌(panic)。

我们完全可以不用全局的flag.CommandLine变量,自己创建一个私有的命令参数容器:

var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)

然后把对flag.StringVar的调用替换为对cmdLine.StringVar调用,把flag.Parse()替换为cmdLine.Parse(os.Args[1:])。示例代码:

package main

import (
	"flag"
	"fmt"
	"os"
)

var name string

var cmdLine = flag.NewFlagSet("param", flag.ExitOnError)

func init() {
	cmdLine.StringVar(&name, "name", "everyone", "The greeting object.")
}

func main() {
	cmdLine.Parse(os.Args[1:])
	fmt.Printf("Hello, %s!\n", name)
}

Go语言读写JSON数据

示例:

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Address struct {
    Type    string
    City    string
    Country string
}

type VCard struct {
    FirstName string
    LastName  string
    Addresses []*Address
    Remark    string
}
 
func main() {
    pa := &Address{"private", "Aartselaar", "Belgium"}
    wa := &Address{"work", "Boom", "Belgium"}
    vc := VCard{"Jan", "Kersschot", []*Address{pa, wa}, "none"}
    fmt.Println(vc)
    js, _ := json.Marshal(vc)
    fmt.Printf(string(js))

    // err:=ioutil.WriteFile("vcard.json", js,0444)
    // fmt.Println(err)
    file, _ := os.OpenFile("vcard.json", os.O_CREATE|os.O_WRONLY, 0)
    defer file.Close()
    enc := json.NewEncoder(file)
    err := enc.Encode(vc)
    if err != nil {
        fmt.Println(err)
    }
}

vcard.json的内容为:

{"FirstName":"Jan","LastName":"Kersschot","Addresses":[{"Type":"private","City":"Aartselaar","Country":"Belgium"},{"Type":"work","City":"Boom","Country":"Belgium"}],"Remark":"none"}

进阶

Go 字符串类型的内部表示

在标准库的 reflect 包中,可以看到下面的代码:

// $GOROOT/src/reflect/value.go
// StringHeader是一个string的运行时表示
type StringHeader struct {
    Data uintptr
    Len  int
}

可以看到,**string 类型由一个指向底层存储的指针和字符串的长度字段组成的。**在 Go 内存中的存储:

image-20220208181610762

字符串值数据存储在被 Data 指向的底层数组中:

func main() {
	var s = "hello你好"
	// 将string类型变量地址显式转型为reflect.StringHeader
	hdr := (*reflect.StringHeader)(unsafe.Pointer(&s))
	fmt.Printf("字节所在内存地址:0x%x\n", hdr.Data)
	// 获取Data字段所指向的数组的指针
	p := (*[11]byte)(unsafe.Pointer(hdr.Data))
	fmt.Println(p)
}

结果:

字节所在内存地址:0x8df8a1
&[104 101 108 108 111 228 189 160 229 165 189]

map 的内部实现

Go 运行时使用一张哈希表来实现抽象的 map 类型。在编译阶段,Go 编译器会将 Go 语法层面的 map 操作,重写成运行时对应的函数调用。大致的对应关系是这样的:

// 创建map类型变量实例
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
// 插入新键值对或给键重新赋值
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") v是用于后续存储value的空间的地址
// 获取某键的值 
v := m["key"]      → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"]  → v, ok := runtime.mapaccess2(maptype, m, "key")
// 删除某键
delete(m, "key")   → runtime.mapdelete(maptype, m, “key”)

map 类型在 Go 运行时层实现的示意图:

image-20220208214045988

语法层面 map 类型变量(m)一一对应的是 runtime.hmap 的实例。hmap 类型是 map 类型的头部结构(header),它存储了后续 map 类型操作所需的所有信息,包括:

image-20220208214228564

真正用来存储键值对数据的是桶,也就是 bucket,每个 bucket 中存储的是 Hash 值低 bit 位数值相同的元素,默认的元素个数为 BUCKETSIZE(值为 8,在 $GOROOT/src/cmd/compile/internal/gc/reflect.go 中定义,与 runtime/map.go 中常量 bucketCnt 保持一致)。

当某个 bucket(比如 buckets[0]) 的 8 个空槽 slot)都填满了,且 map 尚未达到扩容的条件的情况下,运行时会建立 overflow bucket,并将这个 overflow bucket 挂在上面 bucket(如 buckets[0])末尾的 overflow 指针上,这样两个 buckets 形成了一个链表结构,直到下一次 map 扩容之前,这个结构都会一直存在。

每个 bucket 由三部分组成,从上到下分别是 tophash 区域、key 存储区域和 value 存储区域。

key 存储区域

tophash 区域下面是一块连续的内存区域,存储的是这个 bucket 承载的所有 key 数据。运行时在分配 bucket 的时候需要知道 key 的 Size。

当我们声明一个 map 类型变量,比如 var m map[string]int 时,Go 运行时就会为这个变量对应的特定 map 类型,生成一个 runtime.maptype 实例。如果这个实例已经存在,就会直接复用。maptype 实例的结构是这样的:

type maptype struct {
    typ        _type
    key        *_type
    elem       *_type
    bucket     *_type // internal type representing a hash bucket
    keysize    uint8  // size of key slot
    elemsize   uint8  // size of elem slot
    bucketsize uint16 // size of bucket
    flags      uint32
}

这个实例包含了我们需要的 map 类型中的所有"元信息"。前面说到编译器会把语法层面的 map 操作重写成运行时对应的函数调用,这些运行时函数都有一个共同的特点,那就是第一个参数都是 maptype 指针类型的参数。

**Go 运行时就是利用 maptype 参数中的信息确定 key 的类型和大小的。**map 所用的 hash 函数也存放在 maptype.key.alg.hash(key, hmap.hash0) 中。

value 存储区域

key 存储区域下方的另外一块连续的内存区域存储的是 key 对应的 value。和 key 一样,这个区域的创建也是得到了 maptype 中信息的帮助。Go 运行时采用了把 key 和 value 分开存储的方式,而不是采用一个 kv 接着一个 kv 的 kv 紧邻方式存储,这带来的其实是算法上的复杂性,但却减少了因内存对齐带来的内存浪费。

以 map[int8]int64 为例,看看下面的存储空间利用率对比图:

image-20220208215936883

可以看到,当前 Go 运行时使用的方案内存利用效率很高,而 kv 紧邻存储的方案在 map[int8]int64 这样的例子中内存利用率是 72/128=56.25%,有近一半的空间都浪费掉了。

如果 key 或 value 的数据长度大于一定数值,那么运行时不会在 bucket 中直接存储数据,而是会存储 key 或 value 数据的指针。目前 Go 运行时定义的最大 key 和 value 的长度是这样的:

// $GOROOT/src/runtime/map.go
const (
    maxKeySize  = 128
    maxElemSize = 128
)

map 扩容

map在使用过程中,当插入元素个数超出一定数值后,map 会自动扩容,即扩充 bucket 的数量,并重新在 bucket 间均衡分配数据。

Go 运行时的 map 实现中引入了一个 LoadFactor(负载因子),当 count > LoadFactor * 2^B 或 overflow bucket 过多时,运行时会自动对 map 进行扩容。Go 1.17 版本 LoadFactor 设置为 6.5(loadFactorNum/loadFactorDen)。与 map 扩容相关的部分源码:

// $GOROOT/src/runtime/map.go
const (
  ... ...
  loadFactorNum = 13
  loadFactorDen = 2
  ... ...
)
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  ... ...
  if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
    hashGrow(t, h)
    goto again // Growing the table invalidates everything, so try again
  }
  ... ...
}

如果是因为 overflow bucket 过多导致的“扩容”,实际上运行时会新建一个和现有规模一样的 bucket 数组,然后在 assign 和 delete 时做排空和迁移。

如果是因为当前数据数量超出 LoadFactor 指定水位而进行的扩容,那么运行时会建立一个两倍于现有规模的 bucket 数组,但真正的排空和迁移工作也是在 assign 和 delete 时逐步进行的。

原 bucket 数组会挂在 hmap 的 oldbuckets 指针下面,直到原 buckets 数组中所有数据都迁移到新数组后,原 buckets 数组才会被释放。

image-20220208221101805

map 在扩容过程中 value 位置可能发生变化,所以 Go 不允许获取 map 中 value 的地址,这个约束在编译期间就生效了

p := &m[key]  // cannot take the address of m[key]
fmt.Println(p)

结构体类型的内存布局

理想情况下结构体存放在一个连续内存块中,例如类型 T 的内存布局:

image-20220209193545432

结构体类型 T 在内存中布局中没有被 Go 编译器插入的额外字段。我们可以借助标准库 unsafe 包提供的函数,获得结构体类型变量占用的内存大小,以及它每个字段在内存中相对于结构体变量起始地址的偏移量:

var t T
unsafe.Sizeof(t)      // 结构体类型变量占用的内存大小
unsafe.Offsetof(t.Fn) // 字段Fn在内存中相对于变量t起始地址的偏移量

在真实情况下,结构体字段中间可能存在“缝隙”,它们是 Go 编译器插入的“填充物(Padding)”,这是为了满足内存对齐的要求。

比如,一个 int64 类型的变量的内存地址,应该能被 int64 类型自身的大小,也就是 8 整除;一个 uint16 类型的变量的内存地址,应该能被 uint16 类型自身的大小,也就是 2 整除。

对于结构体而言,除了每个字段的内存地址都严格满足内存对齐要求,还需要它的变量的内存地址是最长字段长度与系统对齐系数较小者的整数倍。即先对齐结构体的各个字段,再对齐整个结构体

对于结构体类型 T :

type T struct {
    b byte
    i int64
    u uint16
}

整个计算过程分为两个阶段。第一个阶段是对齐结构体的各个字段。

第一个字段 b 是长度 1 个字节的 byte 类型变量,这样字段 b 放在任意地址上都可以被 1 整除,所以它是天生对齐的。

第二个字段 i 是长度为 8 个字节的 int64 类型变量。按照内存对齐要求,它被放在可以被 8 整除的地址上。如果把 i 紧邻 b 进行分配,当 i 的地址可以被 8 整除时,b 的地址就无法被 8 整除。这时需要在 i 与 b 之间填充了 7 个字节,使得 i 的地址可以被 8 整除时,b 的地址也始终可以被 8 整除。

第三个字段 u,它是一个长度为 2 个字节的 uint16 类型变量,按照内存对其要求,它被放在可以被 2 整除的地址上。i 之后的那个字节的地址肯定可以被 8 整除,也一定可以被 2 整除。将 u 与 i 相邻而放,中间不需要填充。

第二个阶段是对齐整个结构体。

前面说到,结构体的内存地址 = N*min(结构体最长字段的长度, 系统内存对齐系数)

其中N为正整数。这里结构体 T 最长字段 i 的长度为 8,而 64位系统的对齐系数一般为 8,所以整个结构体的对齐系数是 8,即整个结构体的内存地址是8的整数倍。

这要求 T 类型的数组的中的每个元素的地址也要被8 整除,故还需填充6个字节。意味着最终每个字段占用内存空间都是8的整数倍,24就是类型 T 的最终大小。

**为什么会出现内存对齐的要求呢?**这是出于对处理器存取数据效率的考虑。在早期的一些处理器中,比如 Sun 公司的 Sparc 处理器仅支持内存对齐的地址,如果它遇到没有对齐的内存地址,会引发段错误,导致程序崩溃。我们常见的 x86-64 架构处理器虽然处理未对齐的内存地址不会出现段错误,但数据的存取性能也会受到影响。

由于结构体类型的大小受内存对齐约束的影响,导致不同的字段排列顺序也会影响到“填充字节”的多少:

type T struct {
    b byte
    i int64
    u uint16
}
type S struct {
    b byte
    u uint16
    i int64
}
func main() {
    var t T
    println(unsafe.Sizeof(t)) // 24
    var s S
    println(unsafe.Sizeof(s)) // 16
}

前面例子中的内存填充由编译器自动完成的,但也支持主动填充,比如 runtime 包中的 mstats 结构体:

// $GOROOT/src/runtime/mstats.go
type mstats struct {
    ... ...
    // Add an uint32 for even number of size classes to align below fields
    // to 64 bits for atomic operations on 32 bit platforms.
    _ [1 - _NumSizeClasses%2]uint32 // 这里做了主动填充
    last_gc_nanotime uint64 // last gc (monotonic time)
    last_heap_inuse  uint64 // heap_inuse at mark termination of the previous GC
    ... ...
}

通常会通过空标识符来进行主动填充,这是为了保证某个字段的内存地址有更为严格的约束。

方法的本质

Go 语言的方法的本质依然是函数,以下面的类型T为例:

type T struct { 
    a int
}
func (t T) Get() int {  
    return t.a 
}
func (t *T) Set(a int) int { 
    t.a = a 
    return t.a 
}

类型 T 和 *T 的方法可以分别等价转换为下面的普通函数:

// 类型T的方法Get的等价函数
func Get(t T) int {  
    return t.a 
}
// 类型*T的方法Set的等价函数
func Set(t *T, a int) int { 
    t.a = a 
    return t.a 
}

这种等价转换后的函数的类型就是方法的类型,由 Go 编译器自动完成。

正常的调用方式:

var t T
t.Get()
t.Set(1)

也等价于:

var t T
T.Get(t)
(*T).Set(&t, 1)

这种直接以类型名 T 调用方法的表达方式,被称为 Method Expression。Go 语言中的方法的本质就是,一个以方法的 receiver 参数作为第一个参数的普通函数。

我们可以通过代码看到方法的类型:

func main() {
	var t T
	f1 := (*T).Set  // f1的类型,也是T类型Set方法的类型:func (t *T, int)int
	f2 := T.Get     // f2的类型,也是T类型Get方法的类型:func(t T)int
	fmt.Printf("%T\n", f1) // func(*main.T, int) int
	fmt.Printf("%T\n", f2) // func(main.T) int
	f1(&t, 3)
	fmt.Println(f2(t)) // 3
}

接口类型

在 Go 语言中,接口类型无法被实例化的。我们既不能通过调用new函数或make函数创建出一个接口类型的值,也无法用字面量来表示一个接口类型的值。

对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不可能存在。

接口类型的类型字面量与结构体类型的看起来有些相似,构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义,这些方法所代表的就是该接口的方法集合。

对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部的方法,那么它就一定是这个接口的实现类型。

比如定义一个接口如下:

type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}

只要一个数据类型的方法集合中有这 3 个方法,那么它就一定是Pet接口的实现类型。

比如Dog结构体实现了这三个方法:

type Dog struct {
	name string // 名字。
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func (dog Dog) Category() string {
	return "狗"
}

由于SetName是对指针类型*Dog的实现,这相当于Dog类型本身的方法集合中只包含了 2 个方法(所有的值方法)。而它的指针类型*Dog方法集合却包含了 3 个方法。所以只有*Dog指针是Pet接口的实现类型,而Dog不是Pet接口的实现类型。

测试一下:

package main

import "fmt"

type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}

type Dog struct {
	name string // 名字。
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func (dog Dog) Category() string {
	return "狗"
}

func main() {
	// 分别判断 Dog类型 和 *Dog类型
	dog := Dog{"哈士奇"}
	_, ok := interface{}(dog).(Pet)
	fmt.Println("Dog类型是pet接口的实现类型吗?", ok)
	_, ok = interface{}(&dog).(Pet)
	fmt.Println("*Dog类型是pet接口的实现类型吗?", ok)

	// 为接口变量赋值,并调用接口方法
	var pet Pet = &dog
	fmt.Println(pet.Name(), pet.Category())
}

结果:

Dog类型是pet接口的实现类型吗? false
*Dog类型是pet接口的实现类型吗? true
哈士奇 狗

上面我们将 *Dog类型 赋值给了接口变量pet,那么当我们为一个接口变量赋值时会发生什么呢?

首先,我们需要必须清楚变量赋值的是该值的一个副本,而不是该值本身。

例如:

dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")

调用dog的SetName方法后,pet变量的字段name的值依然是"little pig"

dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "monster"

这时的dog2name仍然是"little pig"

其实把dog的值赋给了pet接口,pet接口的值与dog的值是不同的。当我们给一个接口变量赋值的时候,该变量的动态类型会与它的动态值一起被存储在一个专用的数据结构中,可以在源码go/src/runtime/runtime2.go中看到其结构:

// 带有方法的接口
type iface struct {
    // 存储_type信息还有结构实现方法的集合
    tab *itab
    //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
    data unsafe.Pointer
}

// 空接口
type eface struct {
    //类型信息
    _type *_type
    // 指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
    data unsafe.Pointer
}

无论是带方法的接口还是空接口,其底层均存在一个data字段,给接口赋值其实是将被赋值的变量保存到了data字段。当data字段有值时(哪怕为nil),就会认为该接口有意义不再是nil。

所以,接口变量的值其实是这个专用数据结构的一个实例,而不是我们赋给该变量的那个实际的值。对于上述代码的var pet Pet = dog而言,pet的值与dog的值是不同的,无论是存储的内容还是存储的结构。不过,pet的值中包含了dog值的副本。

package main

import (
	"fmt"
	"reflect"
)

type Pet interface {
	Name() string
	Category() string
}

type Dog struct {
	name string // 名字。
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func (dog Dog) Category() string {
	return "dog"
}

func main() {
	var dog1 *Dog
	if dog1 == nil {
		fmt.Println("dog1 nil.")
	} else {
		fmt.Println("dog1 not nil.")
	}
	dog2 := dog1
	if dog2 == nil {
		fmt.Println("dog2 nil.")
	} else {
		fmt.Println("dog2 not nil.")
	}
	var pet Pet = dog2
	if pet == nil {
		fmt.Println("pet nil.")
	} else {
		fmt.Println("pet not nil.")
	}
	fmt.Printf("类型|pet:%T dog:%T\n", pet, dog2)
	fmt.Println("对pet反射得到的类型:", reflect.TypeOf(pet).String())
}

输出:

dog1 nil.
dog2 nil.
The pet is not nil.
类型|pet:*main.Dog dog:*main.Dog
对pet反射得到的类型: *main.Dog

当我们把有类型的nil赋给pet时,Go 语言会用一个iface的实例包装它,包装后的产物就不是nil了,从输出可以看到pet接口确实不为nil。

怎样才能让一个接口变量的值真正为nil呢?要么只声明它但不做初始化,要么直接把字面量nil赋给它。

指针的有限操作

传统意义上的指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。

在 Go 语言中的“指针”还包括uintptr类型和unsafe包的Pointer类型。

uintptr类型根据当前计算机的计算架构的不同,它可以存储 32 位或 64 位的无符号整数,可以代表任何指针的位(bit)模式,也就是原始的内存地址。

unsafe.Pointer可以表示任何指向可寻址的值的指针,同时它也是指针值和uintptr值之间的桥梁。转换关系如下:

image-20211210191453337

unsafe.Pointer可以表示任何指向可寻址的(addressable)值的指针,Go 语言中的哪些值是不可寻址的呢?

以下列表中的值都是不可寻址的:

  • 常量的值。

    const num = 123
    _ = &num // 报错,常量不可寻址。
    
  • 基本类型值的字面量。

    _ = &(123) // 报错,基本类型值的字面量不可寻址。
    
  • 算术操作的结果值。

    _ = &(123 + 456) // 算术操作的结果值不可寻址。
    num2 := 456
    _ = &(num + num2) // 算术操作的结果值不可寻址。
    
  • 各种字面量的索引和切片表达式的结果值,对切片字面量的索引结果值却是可寻址的。

    //_ = &([3]int{1, 2, 3}[0]) // 对数组字面量的索引结果值不可寻址。
    //_ = &([3]int{1, 2, 3}[0:2]) // 对数组字面量的切片结果值不可寻址。
    _ = &([]int{1, 2, 3}[0]) // 对切片字面量的索引结果值却是可寻址的。
    //_ = &([]int{1, 2, 3}[0:2]) // 对切片字面量的切片结果值不可寻址。
    
  • 对字符串变量的索引表达式和切片表达式的结果值。

    //_ = &(str[0]) // 对字符串变量的索引结果值不可寻址。
    //_ = &(str[0:2]) // 对字符串变量的切片结果值不可寻址。
    
  • 对字典变量的索引表达式的结果值。

    var map1 = map[int]string{1: "a", 2: "b", 3: "c"}
    // _ = &(map1[2]) // 对字典变量的索引结果值不可寻址。
    
  • 函数字面量和方法字面量,以及对它们的调用表达式的结果值。

    //_ = &(func(x, y int) int {
    //	return x + y
    //}) // 字面量代表的函数不可寻址。
    //_ = &(fmt.Sprintf) // 标识符代表的函数不可寻址。
    //_ = &(fmt.Sprintln("abc")) // 对函数的调用结果值不可寻址。
    
  • 结构体字面量的字段值。

    dog := Dog{"little pig"}
    //_ = &(dog.Name) // 标识符代表的函数不可寻址。
    //_ = &(dog.Name()) // 对方法的调用结果值不可寻址。
    //_ = &(Dog{"little pig"}.name) // 结构体字面量的字段不可寻址。
    
  • 类型转换表达式的结果值。

    //_ = &(interface{}(dog)) // 类型转换表达式的结果值不可寻址。
    
  • 类型断言表达式的结果值。

    dogI := interface{}(dog)
    //_ = &(dogI.(Named)) // 类型断言表达式的结果值不可寻址。
    
  • 接收表达式的结果值。

    var chan1 = make(chan int, 1)
    chan1 <- 1
    //_ = &(<-chan1) // 接收表达式的结果值不可寻址。
    

常量的值总是会被存储到一个确切的内存区域中,并且这种值肯定是不可变的。基本类型值的字面量也可以被视为常量,只是没有任何标识符可以代表它们。

由于 Go 语言中的字符串值是不可变的,所以基于它的索引或切片的结果值也都是不可寻址的。算术操作的结果值属于一种临时结果。这种结果值赋给任何变量或常量之前,即使能拿到它的内存地址也是没有任何意义的。

Go 语言中的表达式常用的包括以下几种:

  • 用于获得某个元素的索引表达式。
  • 用于获得某个切片(片段)的切片表达式。
  • 用于访问某个字段的选择表达式。
  • 用于调用某个函数或方法的调用表达式。
  • 用于转换值的类型的类型转换表达式。
  • 用于判断值的类型的类型断言表达式。
  • 向通道发送元素值或从通道那里接收元素值的接收表达式。

以上这些表达式施加在某个值字面量上一般都会得到一个临时结果。比如,对数组字面量和字典字面量的索引结果值,对数组字面量和切片字面量的切片结果值。它们都属于临时结果,都是不可寻址的。

切片表达式总会返回一个新的切片值,新切片值在被赋给变量之前属于临时结果,所以对切片字面量的切片结果值是不可寻址的。

值字面量在还没有与任何变量(或者说任何标识符)绑定之前是没有落脚点的,我们无法以任何方式引用到它们,这样的值就是“临时的”。数组类型或切片类型的变量,那么索引或切片的结果值就都不属于临时结果了,是可寻址的。

对字典类型的变量施加索引表达式,得到的结果值不属于临时结果,可是,这样的值却是不可寻址的。原因是,字典中的每个键 - 元素对的存储位置都可能会变化,而且这种变化外界是无法感知的。

在这种情况下,获取字典中任何元素值的指针都是无意义的,也是不安全的。我们不知道什么时候那个元素值会被搬运到何处,也不知道原先的那个内存地址上还会被存放什么别的东西。所以,这样的值就应该是不可寻址的。

“不安全的”操作很可能会破坏程序的一致性,引发不可预知的错误,从而严重影响程序的功能和稳定性。

函数就是代码是不可变的,所以函数和方法都是不可寻址的。对函数或方法的调用结果值也是不可寻址的,因为它们都属于临时结果。

总结:

  1. 不可变的值不可寻址。常量、基本类型的值字面量、字符串变量的值、函数以及方法的字面量。
  2. 绝大多数被视为临时结果的值都是不可寻址的。算术操作的结果值属于临时结果,针对值字面量的表达式结果值也属于临时结果。对切片字面量的索引结果值虽然也属于临时结果,但却是可寻址的。
  3. 若拿到某值的指针可能会破坏程序的一致性,那么就是不安全的,该值就不可寻址。由于字典的内部机制,对字典的索引结果值的取址操作都是不安全的。另外,获取由字面量或标识符代表的函数或方法的地址显然也是不安全的。
  4. 把临时结果赋给一个变量,那么它就是可寻址的了。

示例:

func New(name string) Dog {
	return Dog{name}
}

New("little pig").SetName("monster")

上述代码会产生两个报错:

cannot call pointer method on New("little pig")
cannot take the address of New("little pig")

由于New函数的调用结果值是不可寻址的,所以无法对它进行取址操作。而SetName是指针方法,对自动对New函数的调用结果值进行取值操作,所以运行时产生报错。

怎样通过unsafe.Pointer操纵可寻址的值?

利用unsafe.Pointer的中转和uintptr的底层操作可以绕过 Go 语言的编译器和其他工具的重重检查,并达到潜入内存修改数据的目的。这并不是一种正常的编程手段,使用它会很危险,很有可能造成安全隐患。

对于指针值和uintptr类型值之间的转换,必须使用unsafe.Pointer类型的值作为中转。

例如:

dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))

转换成uintptr类型后,与unsafe.Offsetof函数搭配使用,我们就可以通过内存偏移访问指定位置:

namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))

unsafe.Offsetof函数用于获取两个值在内存中的起始存储地址之间的偏移量,以字节为单位。最终可以计算出name字段值的起始存储地址。再通过两次类型转换把namePtr的值转换成一个*string类型的值,这样就得到了指向dogPname字段值的指针值。

一旦我们获取到name字段值的起始存储地址namePtr,就能够肆意地改动dogP.name的值,以及周围的内存地址上存储的任何数据了。

可以通过nameP的指针修改dog结构体的数据:

*nameP = "monster"

完整代码:

package main

import (
	"fmt"
	"unsafe"
)

type Dog struct {
	name string
}

func (dog *Dog) SetName(name string) {
	dog.name = name
}

func (dog Dog) Name() string {
	return dog.name
}

func main() {
	dog := Dog{"little pig"}
	dogP := &dog
	dogPtr := uintptr(unsafe.Pointer(dogP))

	namePtr := dogPtr + unsafe.Offsetof(dogP.name)
	nameP := (*string)(unsafe.Pointer(namePtr))
	fmt.Printf("nameP == &(dogP.name)? %v\n",
		nameP == &(dogP.name))
	fmt.Printf("The name of dog is %q.\n", *nameP)

	*nameP = "monster"
	fmt.Printf("The name of dog is %q.\n", dog.name)
	fmt.Println()
}

输出:

nameP == &(dogP.name)? true
The name of dog is "little pig".
The name of dog is "monster".

学学C语言的指针的都知道,我们可以使用任意类型去解释一个指针,获取解释后的值:

numP := (*int)(unsafe.Pointer(namePtr))
fmt.Println(*numP)

深入理解用户级线程 goroutine

体现Go 语言最重要的编程哲学和并发编程模式的一句话是:

Don’t communicate by sharing memory; share memory by communicating.

意思是:不要通过共享数据来通讯,要以通讯的方式共享数据。

在Go语言中,不同的 goroutine 之间通过通道以通讯的方式共享数据。我们称操作系统提供的线程叫系统级线程,而 goroutine 代表着并发编程模型中的用户级线程。

用户级线程指的是架设在系统级线程之上的,由用户完全控制的代码执行流程。用户级线程的创建、销毁、调度、状态变更以及其中的代码和数据都完全需要我们的程序自己去实现和处理。

用户级线程的创建和销毁并不用通过操作系统去做,所以速度会很快,不用等着操作系统去调度它们的运行,所以往往会很容易控制并且可以很灵活。缺点在于复杂,系统级线程只要指明需要新线程执行的代码片段,并且下达创建或销毁线程的指令就好了,其他的一切具体实现都会由操作系统代劳。用户则必须全权自己实现,还必须与操作系统正确地对接。

不过Go 语言不但有着独特的并发编程模型,和用户级线程 goroutine,还拥有强大的用于调度 goroutine、对接系统级线程的调度器。

这个调度器是 Go 语言运行时系统的重要组成部分,它主要负责统筹调配 Go 并发编程模型中的三个主要元素,即:G(goroutine)、P(processor)和 M(machine)。其中的 M 指代的是系统级线程。而 P 指的是一种可以承载若干个 G,且能够使这些 G 适时地与 M 进行对接,并得到真正运行的中介。

一个简化的场景:

image-20211211111935228

从宏观上说,G 和 M 由于 P 的存在可以呈现出多对多的关系。当一个正在与某个 M 对接并运行着的 G,需要因某个事件(比如等待 I/O 或锁的解除)而暂停运行的时候,调度器总会及时把这个 G 与那个 M 分离开,以释放计算资源供那些等待运行的 G 使用。

而当一个 G 需要恢复运行的时候,调度器又会尽快地为它寻找空闲的计算资源(包括 M)并安排运行。另外,当 M 不够用时,调度器会帮我们向操作系统申请新的系统级线程,而当某个 M 已无用时,调度器又会负责把它及时地销毁掉。

以下代码会启动一个goroutine

package main

import "fmt"

func main() {
	for i := 0; i < 10; i++ {
		go func() {
			fmt.Println(i)
		}()
	}
}

每一个独立的 Go 程序在运行时总会有一个主 goroutine。这个主 goroutine 会在 Go 程序的运行准备工作完成后被自动地启用。由于主 goroutine 中的代码执行完毕,当前的 Go 程序就会结束运行。所以上述程序一般情况下没有任何输出。

程序执行到一条go语句时,已存在的 goroutine 总是会被优先复用。runtime会先尝试从某个存放空闲的 G 的队列中获取一个 G(goroutine),只有在找不到空闲 G时才会去创建一个新的 G。

G相当于为需要并发执行代码片段服务的上下文环境,创建它的成本也非常低,在runtime内部即可实现。在拿到了一个空闲的 G 之后,runtime会用这个 G 去包装被运行的函数,然后再把这个 G 追加到某个存放可运行的 G 的队列中。

P队列中的G按照先入先出的顺序,由runtime内部的调度器安排运行。

如何让主 goroutine 等待其他 goroutine 呢?

最简单的方法就是主goroutine 结束前阻塞输入或睡眠一段时间,例如:

// 等待回车从而阻塞
var tmp string
fmt.Scanln(&tmp)
// 睡眠1秒
time.Sleep(time.Millisecond * 1000)

另一种方式是通道:

package main

import "fmt"

func main() {
	num := 10
	sign := make(chan struct{}, num)

	for i := 0; i < num; i++ {
		go func(i int) {
			fmt.Println(i)
			sign <- struct{}{}
		}(i)
	}
	for j := 0; j < num; j++ {
		<-sign
	}
}

上述代码在声明通道sign的时候是以chan struct{}作为其类型的。其中的类型字面量struct{}有些类似于空接口类型interface{},它代表了既不包含任何字段也不拥有任何方法的空结构体类型。

struct{}{}占用的内存空间是0字节,并且在整个 Go 程序中永远都只会存在一份。

最终我这次运行的输出为:

9
1
5
0
7
2
3
6
8
4

其实go语言中还有一个工具sync.WaitGroup 可以达到这个目的。

WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为nDone() 每次把计数器-1wait() 会阻塞代码的运行,直到计数器地值减为0

使用WaitGroup 将上述代码可以修改为:

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := sync.WaitGroup{}
	wg.Add(10)
	for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Println(i)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

也会无顺序的打印出0-9。

那么怎样让多个 goroutine 按照既定的顺序运行呢?

一种比较良好的解决方案如下:

package main

import (
	"fmt"
	"sync/atomic"
	"time"
)

var tmp string

func main() {
	var count uint32
	for i := uint32(0); i < 10; i++ {
		go func(i uint32) {
			for {
				if n := atomic.LoadUint32(&count); n == i {
					fmt.Println(i)
					atomic.AddUint32(&count, 1)
					break
				}
				time.Sleep(time.Millisecond)
			}
		}(i)
	}
	fmt.Println("回车结束程序阻塞")
	fmt.Scanln(&tmp)
}

结果成功的按顺序打印出来0-9。

一个循环会不断地获取一个名叫count的变量的值,并判断该值是否与参数i的值相同。如果相同,就立即调用目标,并将count变量的值加1,最后退出当前的循环,结束当前的goroutine。否则先让当前的 goroutine“睡眠”一毫秒再进入下一个循环检查当前count是否已经等于当前goroutine的执行编号。

很明显多个goroutine会共同操作非本地变量count,产生竞态条件(race condition),解决多线程并发问题,我们往往采用加锁或使用原子操作类的方式。sync/atomic包中声明了很多用于原子操作的函数。

runtime包中提供的一些函数:

方法说明
func GOMAXPROCS(n int) int设置最大可同时执行的最大CPU数
func NumCPU() int获取本机CPU个数
func NumGoroutine() int获取当前存在的goroutine数量
func GC()立即执行一次垃圾回收
func SetFinalizer(x, f interface{})给变量绑定方法,垃圾回收时执行
func NumCgoCall() int64获取当前进程执行的cgo调用次数
  • 58
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 41
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交