下载地址:https://gitcode.net/as604049322/blog_pdf

安装与运行环境

Go 语言环境安装

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

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

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

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

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

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

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

>go version
go version go1.17.3 windows/amd64

输入go env查看Go配置:

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

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

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

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

运行环境runtime:

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

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

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

常用命令

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

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

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

效果如下:

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

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

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

Goland开发工具安装

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

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

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

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

Go语言的三个重要演进

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

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

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

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

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

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

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

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

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

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

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

Go项目的布局标准

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最简化的布局:

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

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

Go语言基础入门

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

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

break default func interface
case defer go map
chan else goto package
const fallthrough if range
continue for import return

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

append bool byte cap close complex
copy false float32 float64 imag int
int32 int64 iota len make new
print println real recover string true

hello word

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

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

然后再控制台执行:

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

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

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

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

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

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

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

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

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

import "fmt"
import "os"

或:

import "fmt"; import "os"

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

import ("fmt""os"
)

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

import ("fmt"; "os")

包的别名:

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

Go 程序的启动顺序如下:

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

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

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

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

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

报错信息为:undefined: init

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

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

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

Go语言的构建模式

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

GOPATH环境变量

Go语言的三个环境变量:

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

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

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

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

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

import "github.com/labstack/echo"

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

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

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

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

大致结构如下:

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

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

库源码文件

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

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

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

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

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

package mainimport "flag"var name stringfunc init() {flag.StringVar(&name, "name", "everyone", "The greeting object.")
}func main() {flag.Parse()hello(name)
}

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

package mainimport "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 tidyE:\go_project\pack>go build packE:\go_project\pack>pack.exe
Hello, everyone!

分包示例:

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

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

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

package mainimport ("flag""pack/lib"
)var name stringfunc init() {flag.StringVar(&name, "name", "everyone", "The greeting object.")
}func main() {flag.Parse()lib.Hello(name)
}

运行结果:

>go run pack2.go
Hello, everyone!

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

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

GOPATH 构建模式

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

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

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

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

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

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

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

>go env -w GOPATH=E:\go_lib

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

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

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

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

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

vendor 机制

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

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

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

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

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

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

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

Go Module 构建模式

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

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

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

首先创建go.mod 文件:

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

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

module testgo 1.17

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

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

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

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

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

module testgo 1.17require github.com/sirupsen/logrus v1.8.1require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect

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

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

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

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

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


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

Go Module 的语义导入版本机制

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

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

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

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

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

import "github.com/sirupsen/logrus"

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

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

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

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

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

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

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

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

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

Go Module 的最小版本选择原则

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

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

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

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

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

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

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

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

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

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 testgo 1.17require (github.com/google/uuid v1.3.0github.com/sirupsen/logrus v1.8.1
)require golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 // indirect

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

移除一个依赖

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

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

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

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

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

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

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

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

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

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

特殊情况:使用 vendor

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

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

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

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

基本语法

注释与字符串格式化

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

go语言的注释规则如下:

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

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

字符串格式化:

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

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

Go 字符串格式化符号:

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

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

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

结果:

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

例如:

package mainimport "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 mainimport "fmt"func main() {fmt.Printf("|%6d|%6d|\n", 12, 345)fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)fmt.Printf("|%6s|%6s|\n", "foo", "b")fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
}
|    12|   345|
|  1.20|  3.45|
|1.20  |3.45  |
|   foo|     b|
|foo   |b     |

格式化函数的用法:

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

Go 语言运算符

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

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

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

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

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

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

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

Go 语言条件语句

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

if 语句 的完整语法如下:

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

示例:

package mainimport "fmt"func main() {num := 99if 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 mainimport "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:// 执行分支1case expr2:// 执行分支2case expr3_1, expr3_2, expr3_3:// 执行分支3case expr4:// 执行分支4... ...case exprN:// 执行分支Ndefault: // 执行默认分支
}

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

package mainimport "fmt"func main() {/* 定义局部变量 */var grade string = "B"var marks int = 90switch 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")fallthroughcase case2():println("exec case2")fallthroughdefault:println("exec default")}
}

结果:

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

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

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

Go 语言循环语句

For 循环的完整形式如下:

for init; condition; post { }

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

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

示例:

package mainimport "fmt"func main() {sum,i := 0,1for ; 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])}// 迭代mapkvs := 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 mainimport "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 slicefor i := 0; i < len(sl); i++ {switch sl[i] % 2 {case 0:firstEven = sl[i]breakcase 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 loopcase 1:// do nothing}}println(firstEven) // 6
}

for-range和switch语句中的细节

for-range示例:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

for-range示例:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Go 语言数据类型

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

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

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

整型主要有:

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

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

浮点型包括 float32float64

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

复数包括complex64complex128

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

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

派生类型包括:

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

数值字面值(Number Literal)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

浮点型的二进制表示

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

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

符号位(S) 阶码(E) 尾数(M)
sign exponent maintissa

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

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

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

所占 bit 位数 符号位(S) 阶码(E) 尾数(M)
单精度float32 1 8 23
双精度float64 1 11 52

对于 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)
0 10000110 00010111101000000000000

对于 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.8125fmt.Printf("%b\n", math.Float32bits(f1))var f2 = 139.8125fmt.Printf("%b\n", math.Float64bits(f2))
}

结果:

1000011000010111101000000000000
100000001100001011110100000000000000000000000000000000000000000

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

与字符相关的数字类型

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

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

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

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

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

package mainimport ("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 - β - 												

决战Go语言从入门到入土v0.1相关推荐

  1. 为什么c语言会入门到入土,大整数从入门到入土

    大整数(高精) 声明,这是一篇从入门到入土的大整数讲解,可能有那么一点小长,内容涵盖大整数加法,减法和乘法,但由于水平有限,还是有不足之处希望评论提出.如果你是一个萌新,相信我,我会写得非常详细,你一 ...

  2. C语言从入门到入土(三)

    前言 生活还要继续,学习不能停止,本期就给大家大致了解一下函数,数组的基本使用,以及一些常见操作符的认识,方便大家能更好的看懂大佬写的代码. 目录 前言 一.函数 1.1 库函数 1.2 自定义函数 ...

  3. (初识C语言p2) C语言从入门到入土(入门篇)

    初识C语言 什么是C语言 第一个C语言程序 数据类型 变量.常量 字符串+转义字符+注释 //以上是上一节讲过的想要观看的小伙伴们可以去看之前的博客哟 目录 初识C语言 6. 选择语句 7. 循环语句 ...

  4. R语言从入门到入土--一文带你解锁ggplot2绘图大法

    一.准备工作 (1)下载包:install.packages(tidyverse)--注意选择国内源 (2)载入包:library(tidyverse) (3)了解一下本次实验所使用的几个数据包 可以 ...

  5. 【C语言从入门到入土】——“井字棋”

    目录 1.游戏规则 2.设计思路 1.棋盘的初始化 2.打印棋盘 3.玩家操作 4.电脑下棋 6.输赢判断 7.完整代码展示 1.游戏规则 井字棋又叫三子棋,作为童年经典小游戏,相信各位无论是在上课摸 ...

  6. C语言祸根看不见的错误,那些年学指针从入门到入土都是内存问题

    是那样的爱学习 那一刻从入门到入土 丑拒 前言 C 语言程序中的内存错误非常有害:它们很常见,并且可能导致严重的后果,最难受的极大多数时候内存问题看不见,也摸不着.编译正确运行出错,让新手从入门到入土 ...

  7. 【C语言进阶】从入门到入土(数据的存储)

    前言: 这一篇我们来深度剖析数据在内存中的存储,让我们走进数据在内存中到底是任何进行存储的,不同的数据类型有何差异. 进阶之数据的存储: 一.数据类型详细介绍及归类 1.数据类型介绍 2.类型的基本归 ...

  8. R语言|for循环————R语言入门到入土系列(八)

    R语言入门到入土系列   R语言作为数据科学的第一利器:本人介绍了一些R语言的基础入门知识,希望能帮到大家,往期系列文章点击下面文字直达

  9. 【从入门到入土系列】C语言制作小游戏-贪吃蛇:Copy+运行即可另附注释

    系列文章 本系列持续更新中,欢迎您的访问! 系列简介 本系列由唐文疏撰写,负责记录博主的学习生涯中的一点一滴.独乐乐不如众乐乐,故此分享给大家.欢迎大家一起讨论.学习和批评指点. 博主只是一个普普通通 ...

最新文章

  1. Ubuntu14.04安装GitLab指南
  2. 023_Jedis的发布和订阅
  3. JAVA基础:Hibernate外键关联与HQL语法
  4. 移动应用可以通过微信沟通接口连接公众号 微信涨粉多了一个新通道
  5. python计算while循环次数_python-----运算符及while循环
  6. P1421 小玉买文具【入门题】
  7. 从零实现Vue的组件库(零)- 基本结构以及构建工具
  8. CakePHP中文手册【翻译】
  9. ZOJ 2760 How Many Shortest Path 最大流+floyd求最短路
  10. paper reading:高光谱遥感影像空谱特征提取与分类方法研究 - 康旭东
  11. Matlab之矩阵的特征值与特征向量求解
  12. python创建一个文本文件_利用python如何实现创建一个文本文件
  13. Python 结构体数组初始化代码示例
  14. JavaSE银行账户管理系统
  15. word中mathtype的公式后面的光标位置
  16. [渝粤教育] 西南科技大学 英语泛读 在线考试复习资料
  17. BRC4工具1.2.2入门教程学习
  18. IDEA Eval Reset 使用方法
  19. 刺沙--冰雪传奇自动回收,使用元宝经验卷轴(手机按键)
  20. 谈谈自己对文件描述符的理解

热门文章

  1. slqite3库查询数据处理方式_C语言实现sqlite3数据库查询的基本方法
  2. Web端调用本地打印机-Java实现
  3. 魅族手机文件删除-通知栏警告流程分析(上)
  4. 将docker应用并发布到远程服务器
  5. 奥曲肽修饰四氧化三铁Oct-Fe3O4/穿膜肽修饰Fe3O4/金壳纳米复合物的制备
  6. Pacemaker,Corosync和PCS搭建高可用性负载均衡linux 集群(httpd)
  7. 诗圣杜甫不同时期的代表作
  8. 转战掘金 我的年度总结
  9. python怀旧特效代码
  10. Pon De Replay【MMD动作+镜头下载】