在这里插入图片描述
下载地址:/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 env
...
set GOPROXY=,direct
set GOROOT=D:\deploy\go
...
GOPROXY=,direct

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

go env -w GO111MODULE=on
go env -w GOPROXY=,direct

运行环境runtime:

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

$GOROOT/src/runtime

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

常用命令

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

go buildgo installgo run
gofmtgofmt –w program.go-w

效果如下:

image-20211202122637737

go doc
go doc packagego doc fmtgodocfmtgo doc package/subpackagego doc container/listgo doc package functiongo doc fmt Printffmt.Printf()
godocgodoc

Goland开发工具安装

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

首先到/zh-cn/go/download/other.html选择一个合适的版本下载,这里我下载了/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 (
  /x/crypto v0.0.0-20190611184440-5c40567a22f8
  /x/net v0.0.0-20190813141303-74dc4d7220e7
  /x/sys v0.0.0-20190529130038-5219a1e1c5f8 // indirect
  /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语言基础入门

推荐一个在线网站:/list

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

breakdefaultfuncinterface
casedefergomap
chanelsegotopackage
constfallthroughifrange
continueforimportreturn

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

appendboolbytecapclosecomplex
copyfalsefloat32float64imagint
int32int64iotalenmakenew
printprintlnrealrecoverstringtrue

hello word

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
package mainmain
import "fmt"fmtfmt""

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

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/echogo install github.com/labstack/echogithub.com/labstack/echo.a

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

大致结构如下:

image-20211208213139403

go buildgo install

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

库源码文件

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

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

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

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

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)
}
hellopack1_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!

分包示例:

pack2_lib.go
package lib

import "fmt"

func Hello(name string) {
	fmt.Printf("Hello, %s!\n", name)
}
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 语言自然地把程序实体的访问权限划分为包级私有和公开的。

internalinternalinternal

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 /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/
    └── /
        └── x/
            └── sys/
                └── unix/

添加完 vendor 后重新编译 test.go,这个时候 Go 编译器就会在 vendor 目录下搜索程序依赖的 logrus 包以及后者依赖的 /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 modgo 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 /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=
/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
/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 /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)
├──  (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语言的变量名由字母数字和下划线组成,第一个字符必须是下划线或字母。

字符串格式化:

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.SprintfPrintffmt.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 { }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")
	}
switchintcaseint8switch

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

	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")
	}
caseswitchint8
switchcase
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")
}
caseswitch
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")
}
switchswitchswitchcase
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)
}
byteuint8byteuint8

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")
	}
switchintcaseint8switch

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

	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")
	}
caseswitchint8
switchcase
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")
}
caseswitch
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")
}
switchswitchswitchcase
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)
}
byteuint8byteuint8

Go 语言数据类型

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

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 文本。

派生类型包括:

*int

数值字面值(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×2E−offset
其中 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 后得到二进制一致。

与字符相关的数字类型

byteuint8
var ch byte = 'A'
var ch byte = 65
var ch byte = '\x41'
runeint32
\u\U
int16int\u\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
unicodech
unicode.IsLetter(ch)unicode.IsDigit(ch)unicode.IsSpace(ch)
utf8
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
)
type_name(expression)
b := int(5.0)

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

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

变量的可见性规则

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

值类型和引用类型

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

&*
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 *intvar 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
func myFunc(a, b, arg ...int) {}

示例函数和调用:

func Greeting(prefix string, who ...string)
Greeting("hello:", "Joe", "Anna", "Eileen")
who[]string{"Joe", "Anna", "Eileen"}
arrarr...

示例:

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

匿名函数与函数变量

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 和追踪

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
}
errorsNewfmt.Errorferror
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.Errorffmt.Sprintferrors.New
err1 := fmt.Errorf("错误内容: %s", "***")
err2 := errors.New(fmt.Sprintf("错误内容: %s", "***"))
fmt.Println(err1.Error() == err2.Error())

结果为true。

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 中。

runtime.ErrorRuntimeError()
panicpanic

一旦 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
recoverrecover
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

切片

[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 函数常见操作:

b = make([]T, len(a))
copy(b, a)
appendappend

切片的扩容策略

1024
10241.25
runtimegrowslice

示例:

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[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},
}
}
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]
slice s[]intlen(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(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)
}
runetype rune = int32int32
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 字符。

stringrune
string

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 类型,支持并发读写。可以参考:/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 literal
或
var 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

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
SquareareaIntf = sq1SquareSquareArea()
SquareArea()
cannot use sq1 (type *Square) as type Shaper in assignment:
*Square does not implement Shaper (missing Area method)
ShaperPerimeter()SquareSquare
RectangleShaperShaperArea()
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
Shaper
ioReader
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)

一个接口可以包含一个或多个其他的接口,这相当于直接将这些内嵌接口的方法列举在外层接口中一样。

FileReadWriteLockClose()
type ReadWrite interface {
    Read(b Buffer) bool
    Write(b Buffer) bool
}

type Lock interface {
    Lock()
    Unlock()
}

type File interface {
    ReadWrite
    Lock
    Close()
}

检测和转换接口变量的类型

varIvarIT
v := varI.(T)
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
vvarIToktruevTokfalse

例如:

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-switchfallthrough

空接口

空接口或者最小接口 不包含任何方法,它对实现不做任何要求:

type Any interface {}
Java/C#Objectvar 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)
}

反射

TypeValue
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
Interface()reflect.ValueOf(x).float()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
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]
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)

<-
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]
syncMutex

读写锁:

sync.RWMutexsync.RWMutexLockUnlockRLockRUnlock

对于某个受到读写锁保护的共享资源,多个写操作不能同时进行,写操作和读操作也不能同时进行,但多个读操作却可以同时进行。

示例:

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())
locksync.NewCondsendCondrecvCond
sendCondrecvCond
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
int32int64uint32uint64uintptrunsafePointer

原子减法

对于int32我们可以直接进行原子减法:

num1 := int32(18)
atomic.AddInt32(&num1, int32(-3))
fmt.Println("num1 =", num1)

输出:15

uint32int32uint32uint32(int32(-3))-3uint32int32(-3)uint32

示例:

num2 := uint32(18)
delta := int32(-3)
atomic.AddUint32(&num2, uint32(delta))
fmt.Println("num2 =", num2)

输出:15

^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。

for
for !atomic.CompareAndSwapInt32(&num, 5, 0) {
	time.Sleep(time.Millisecond * 500)
}
for

使用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
syncWaitGroupOnce
包的
WaitGroupAddDoneWait0
DoneWait
sync.WaitGroupcoordinateWithChan
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

Wait
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.OnceOnceDo
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)
}
OnceDoOncem
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]
goroutineOnceDo

context.Context类型

coordinateWithWaitGroupsync.WaitGroupContext
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.WithCancelcontext.Backgroundcontext.Contextcontext.CancelFunc
cxtDone
addNumcoordinateWithContext

Context的接口定义的比较简洁:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}
Deadline
Donestruct{}
Err
Value
ContextContextBackground
Background
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
WithDeadlineWithTimeout
WithValueContext.Value
ContextDone

image-20211220183224646

context.WithValueContext
ContextValueContextContextContext
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语言的标准库

标准库概述

下面按功能进行分组介绍内置包的简单用途:

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)
	}
}

随机数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.Float64rand.Intn
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

前缀和后缀:

HasPrefixsprefix
strings.HasPrefix(s, prefix string) bool
HasSuffixssuffix
strings.HasSuffix(s, suffix string) bool

字符串包含关系:

Containsssubstr
strings.Contains(s, substr string) bool

获取索引位置:

Indexstrsstrsstr
strings.Index(s, str string) int
LastIndexstrsstrsstr
strings.LastIndex(s, str string) int

字符串替换:

Replacestrnoldnewn = -1oldnew
strings.Replace(str, old, new, n) string

统计出现次数:

Countstrs
strings.Count(s, str string) int

重复字符串:

Repeatcounts
strings.Repeat(s, count int) string

大小写:

ToLower
strings.ToLower(s) string
ToUpper
strings.ToUpper(s) string

trim:

删除首尾空白字符:

 strings.TrimSpace(s) string

删除首尾指定字符(第二个参数表示要删除的字符列表,包含任务一个字符都会被删除),例如:

strings.Trim(s, "cut")
TrimLeftTrimRight

分割字符串:

strings.Split(s, sep)
strings.Fields(s)

拼接 slice 到字符串:

strings.Join(sl []string, sep string) string

例如:

str := []string{"Google","Baidu","Taobao","Weibo"}
strings.Join(str,"|")

更多字符串操作可参考:/pkg/strings/

从字符串中读取内容:

strings.NewReader(str)ReaderReader
Read()ReadByte()ReadRune()
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

更多用法:/pkg/strings/

strings.Builder 与 bytes.Buffer

Builderbyte
Builderunsafe.Pointer
BuilderBuilder
BuilderWriteWriteByteWriteRuneWriteStringBuilderGrowResetBuilder

示例:

	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 
Grownn
strings.Builder
strings.Builder
var builder1 strings.Builder
builder1.Grow(1)
builder3 := builder1
//builder3.Grow(1) // 这里会引发 panic。
strings.Builderio.Writerio.ByteWriterfmt.Stringerio.StringWriter
bytes.Buffer
bytes.Bufferstrings.Builderbytes.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
io.Readerio.ByteReaderio.RuneReaderio.ByteScannerio.RuneScannerio.WriterTo

共有 6 个。而其实现的写入相关的接口则有这些。

io.Writerio.ByteWriterio.stringWriterio.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'precbitSize

将字符串转换为 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包的用法参考:/pkg/strconv/

container包中的容器

container/list
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)
MoveBeforeMoveAfter
MoveToFrontMoveToBack
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
FrontBackInsertBeforeInsertAfterPushFrontPushBack
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.ListlPushFrontPushBackPushBackListPushFrontList

更多用法参考:/pkg/container/list/

RingList
RingListElementRingListRingvar r ring.Ringr1List0RingLenListLen

示例:

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

更多用法参考:/pkg/container/ring/

container包下还有一个数据结构堆:/pkg/container/heap/

并发安全字典sync.Map

sync.Mapmapsync.Mapsync.Map
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

示例:

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 包

math/bigbig.Intbig.Rat
big.NewInt(n)big.NewRat(N,D)
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
DurationLocationUTC
Format

预定义格式有:

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定时器的用法:

结果:

更多用法:/pkg/time/

读写数据

标准输入与输出

os.Stdinfmt

示例:

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
ScanlnScanfSscan
bufio
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)
	}
}
ReadStringnilio.EOFdelimerr != nil
os.Stdoutos.Stderr

文件读取

os.Fileos.Stdinos.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')
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)
}
pathfilepath
import "path/filepath"
filename := filepath.Base(path)
compress

下面的程序展示了如何读取一个 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_RDONLYos.WRONLYos.O_CREATEos.O_TRUNC
OpenFile
fmt.Fprintf(outputFile, “Some test data.\n”)fmtio.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/ioutilioutil.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.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[]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.StringVar
flag.StringVar(&name, "name", "everyone", "The greeting object.")
flag.StringVar
name&namenameeveryone

例如:

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.Usagemain
flag.Usage = func() {
	fmt.Fprintf(os.Stderr, "Usage of %s:\n", "param")
	flag.PrintDefaults()
}
flagStringVarParseflag.CommandLine
flag.CommandLineinit
flag.CommandLine = flag.NewFlagSet("", flag.ExitOnError)
flag.CommandLine.Usage = func() {
	fmt.Fprintf(os.Stderr, "Usage of %s:\n", "param")
	flag.PrintDefaults()
}
flag.NewFlagSet--helpflag.ExitOnError22flag.PanicOnError
flag.CommandLine
var cmdLine = flag.NewFlagSet("question", flag.ExitOnError)
flag.StringVarcmdLine.StringVarflag.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
}

接口类型

newmake

对于某一个接口类型来说,如果没有任何数据类型可以作为它的实现,那么该接口的值就不可能存在。

接口类型的类型字面量与结构体类型的看起来有些相似,构体类型包裹的是它的字段声明,而接口类型包裹的是它的方法定义,这些方法所代表的就是该接口的方法集合。

对于任何数据类型,只要它的方法集合中完全包含了一个接口的全部的方法,那么它就一定是这个接口的实现类型。

比如定义一个接口如下:

type Pet interface {
	SetName(name string)
	Name() string
	Category() string
}
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 "狗"
}
*DogDog*Dog*DogDog

测试一下:

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类型

首先,我们需要必须清楚变量赋值的是该值的一个副本,而不是该值本身。

例如:

dog := Dog{"little pig"}
var pet Pet = dog
dog.SetName("monster")
petname"little pig"
dog1 := Dog{"little pig"}
dog2 := dog1
dog1.name = "monster"
dog2name"little pig"
dogpetpetdog
// 带有方法的接口
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 = dogpetdogpetdog
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
nilpetifacenil
nilnil

指针的有限操作

传统意义上的指针是一个指向某个确切的内存地址的值。这个内存地址可以是任何数据或代码的起始地址,比如,某个变量、某个字段或某个函数。

uintptrunsafePointer
uintptr
unsafe.Pointeruintptr

image-20211210191453337

unsafe.Pointer

以下列表中的值都是不可寻址的:

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")
NewNew
unsafe.Pointer
unsafe.Pointeruintptr
uintptrunsafe.Pointer

例如:

dog := Dog{"little pig"}
dogP := &dog
dogPtr := uintptr(unsafe.Pointer(dogP))
uintptrunsafe.Offsetof
namePtr := dogPtr + unsafe.Offsetof(dogP.name)
nameP := (*string)(unsafe.Pointer(namePtr))
unsafe.OffsetofnamenamePtr*stringdogPname
namenamePtrdogP.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

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
	}
}
signchan struct{}struct{}interface{}
struct{}{}0

最终我这次运行的输出为:

9
1
5
0
7
2
3
6
8
4
sync.WaitGroup
WaitGroupAdd(), 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。

counti
countsync/atomic
runtime
func GOMAXPROCS(n int) intfunc NumCPU() intfunc NumGoroutine() intfunc GC()func SetFinalizer(x, f interface{})func NumCgoCall() int64