Go学习笔记
1. 入门
1.1 Go语言特点
- 静态类型、编译型的开源语言
- 脚本话的语法,支持多种编程范式(函数式 & 面向对象式)
- 原生、给力的并发编程支持
1.2 Go 语言优势
- 脚本化的语法
- 静态类型+编译型,程序运行速度有保障
- 原生的支持并发编程,降低开发维护成本,程序可以更好的执行
1.3 Go语言劣势
- 语法糖没有那么多
- 目前的程序运行速度还不及C,但已经赶超了 C++ 和 Java
- 第三方函数库暂时不像绝对主流的编程语言那样多
1.4 环境变量
GOROOT:Go语言的当前安装目录
1
export GOROOT=/usr/local/go
GOPATH:Go语言的工作区的集合
GOBIN:存放Go程序可执行文件的目录
PATH:方便使用Go语言命令和Go程序的可执行文件
1
export PATH=$PATH:$GOROOT/bin:$GOBIN
GOOS:操作系统
- GOARCH:计算架构
2. 基本规则
2.1 工作区和GOPATH
工作区是放置Go源码文件的目录,一般情况下,Go源码文件都需要存放到工作区中,但是对于命令源码文件来说,这不是必须的。
src目录:存放源码文件,以代码包为组织形式
pkg目录:用户存放归档文件(.a),所有归档文件都会被存放到该目录下的平台相关目录中,同样以代码包为组织形式
平台相关目录:以 $GOOS_GOARCH为命名方式,如:linux_amd64
bin目录:存放当前工作区中的Go程序的可执行文件
2.2 源码文件的分类和含义
Go源码文件,名称以 .go
为后缀,内容以 Go 语言代码组织的文件,多个 Go 源码文件是需要用代码包组织起来的。
源码文件分三类:命令源码文件、库源码文件、测试源码文件
命令源码文件
声明自己属于
main
代码包,包含无参数声明和结果声明的main
函数,被安装后,相应的可执行文件会被存放到 GOBIN 指向的目录或当前工作区目录/bin
下.命令源码文件是 Go 程序的入口,但不建议把程序都写在一个文件中。
注意:同一个代码包中强烈不建议直接包含多个命令源码文件。
库源码文件
不具备命令源码文件的那两个特征的源码文件,被安装后,相应的归档文件会被存放到
当前工作区目录/pkg/平台相关目录
下。测试源码文件
不具备命令源码文件的那两个特征的源码文件,名称以
_test.go
为后缀,其中至少有一个函数的名称以 Test 或 Benchmark 为前缀,并且,该函数接受一个类型为 testing.T 或 testing.B 的参数。1
2
3
4
5
6
7
8
9// 功能测试函数
func TestFind (t *testing.T) {
// xxx
}
// 基准测试函数 或 性能测试函数
func BenchmarkFind (b *testing.B) {
// xxx
}
2.3 代码包(package)
一个代码包实际上就是一个由导入路径代表的目录,导入路径即 工作区目录/src
或 工作区目录/pkg/平台相关目录
之下的某段子路径。
每个源码文件必须声明其所属的代码包,同一个代码包中的所有源码文件声明的代码包应该是相同的。
代码包声明
与代码包导入路径
的区别:代码包声明语句中的包名称应该是该代码包的导入路径的最右子路径,代码包导入语句中使用的包名称应该与其导入路径一致,例如:
1 | import ( |
代码包的导入方法:
带别名的导入,使用别名调用方法
1
import str "strings"
本地化的导入,省略包名直接调用
1
import . "strings"
仅初始化,无法调用
1
import _ "strings"
代码包的初始化:
代码包初始化函数即:无参数声明和结果声明的 init
函数,init
函数可以被声明在任何文件中,且可以有多个,无论某个代码包被引入几次,它的 init
函数只会被执行一次。
init
函数的执行时机:
单一代码包内:对所有全局变量进行求职,执行所有的
init
函数,且执行顺序不确定。不同代码包之间:先执行被导入代码包中的
init
函数,再执行导入它的代码包中的init
函数。导入顺序:a ==> b ==> c
执行顺序:c ==> b ==> a
不应该对在同一个代码包中被导入的多个代码包的
init
函数的执行顺序做假设,比如 a 同时导入 b 和 c,b 和 c 的执行顺序不确定
3. 命令基础
3.1 go run
用于运行命令源码文件,只能接受一个命令源码文件以及若干个库源码文件作为参数
内部操作步骤:先编译源码文件再运行
常用标记:
-a:强制编译相关代码,不来它们的编辑结果是否已是最新的
-n:打印编译过程中所需运行的命令,但不真正执行它们
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15➜ study go run -n 01/main.go
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/Users/Jormin/Library/Caches/go-build/54/54a651063f7c0c35ba45135a233beebb06360c533157ad9af07e634ff7030628-d
packagefile fmt=/usr/local/go/pkg/darwin_amd64/fmt.a
packagefile math=/usr/local/go/pkg/darwin_amd64/math.a
packagefile runtime=/usr/local/go/pkg/darwin_amd64/runtime.a
packagefile errors=/usr/local/go/pkg/darwin_amd64/errors.a
packagefile internal/fmtsort=/usr/local/go/pkg/darwin_amd64/internal/fmtsort.a
...
EOF
mkdir -p $WORK/b001/exe/
cd .
/usr/local/go/pkg/tool/darwin_amd64/link -o $WORK/b001/exe/main -importcfg $WORK/b001/importcfg.link -s -w -buildmode=exe -buildid=WjXgHwJFvZzLk72C8QN1/K14U1WxlRWLEZybque9y/5tHl15Fqf6uVhqHerj3s/WjXgHwJFvZzLk72C8QN1 -extld=clang /Users/Jormin/Library/Caches/go-build/54/54a651063f7c0c35ba45135a233beebb06360c533157ad9af07e634ff7030628-d
WORK/b001/exe/main-p n:并行编译,其中 n 为并行的数量,最好设定为当前计算机 CPU 的逻辑个数(物理CPU个数 * 2)
-v:列出被编译的代码包的名称
-a 和 -v 联用可以列出所有被编译的代码包的名称
v1.3:包含 Go 语言自带的标准库的代码包
v1.4:不包含 Go 语言自带的标准库的代码包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34➜ study go version
go version go1.15.1 darwin/amd64
➜ study go run -a -v 01/main.go
internal/unsafeheader
unicode/utf8
internal/race
unicode
math/bits
runtime/internal/sys
internal/cpu
sync/atomic
runtime/internal/atomic
runtime/internal/math
internal/testlog
internal/bytealg
math
runtime
internal/reflectlite
sync
errors
sort
internal/oserror
io
strconv
syscall
reflect
internal/syscall/execenv
internal/syscall/unix
time
internal/poll
internal/fmtsort
os
fmt
command-line-arguments-work:显示编译时创建的临时工作目录的路径,并且不删除它
1
2
3
4
5
6
7
8
9
10➜ study go run -work 01/main.go
WORK=/var/folders/tb/x1c1w715551c8vf5454n7c680000gn/T/go-build866706324
➜ ~ tree /var/folders/tb/x1c1w715551c8vf5454n7c680000gn/T/go-build866706324
/var/folders/tb/x1c1w715551c8vf5454n7c680000gn/T/go-build866706324
└── b001
├── exe
│ └── main
└── importcfg.link
2 directories, 2 files-x:打印编译过程中所需运行的命令,会执行命令源文件
1
2
3
4
5
6
7
8
9➜ gin go run -x main.go
WORK=/var/folders/tb/x1c1w715551c8vf5454n7c680000gn/T/go-build188413134
mkdir -p $WORK/b001/
cat >$WORK/b001/importcfg.link << 'EOF' # internal
packagefile command-line-arguments=/Users/Jormin/Library/Caches/go-build/a6/a6fab1bafbbddfc2d54fc3096e9785914380735447be0d5412a984bf930bdd63-d
packagefile gin/routers=/Users/Jormin/Library/Caches/go-build/27/27dc05a5998c611043c9c630d23380989ac87ad81b2ab27efc03342a9c3a4d50-d
packagefile github.com/gin-gonic/gin=/Users/Jormin/Library/Caches/go-build/34/3460d4fa6b078b46d95e8d6c67fff181bd64a31753d2f23fc39112b689741172-d
packagefile net/http=/Users/Jormin/Library/Caches/go-build/95/951232e1fd7fabf8f9e694f5b1eb9f3d4cc42fb1de552526b7a0eb617c6ffd6d-d
...
3.2 go build
用于编译源码文件或代码包。
编译非命令源码文件不会产生任何结果文件,仅仅检查库源码文件的有效性。
编译命令源码文件会在该命令的执行目录生成一个可执行文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20➜ gin ll
total 48
drwxr-xr-x 4 jormin staff 128B 9 10 09:54 database
-rw-r--r-- 1 jormin staff 217B 9 10 09:43 go.mod
-rw-r--r-- 1 jormin staff 13K 9 10 09:43 go.sum
-rw-r--r-- 1 jormin staff 330B 9 9 19:22 main.go
drwxr-xr-x 4 jormin staff 128B 9 9 19:19 modules
drwxr-xr-x 5 jormin staff 160B 9 11 14:50 routers
drwxr-xr-x 8 jormin staff 256B 9 10 09:43 vendor
➜ gin go build main.go
➜ gin ll
total 36048
drwxr-xr-x 4 jormin staff 128B 9 10 09:54 database
-rw-r--r-- 1 jormin staff 217B 9 10 09:43 go.mod
-rw-r--r-- 1 jormin staff 13K 9 10 09:43 go.sum
-rwxr-xr-x 1 jormin staff 18M 9 20 19:19 main
-rw-r--r-- 1 jormin staff 330B 9 9 19:22 main.go
drwxr-xr-x 4 jormin staff 128B 9 9 19:19 modules
drwxr-xr-x 5 jormin staff 160B 9 11 14:50 routers
drwxr-xr-x 8 jormin staff 256B 9 10 09:43 vendor如果不追加任何参数,会试图把当前目录作为代码包并编译,但不会产生任何输出,也不会生成可执行文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21➜ gin ll
total 36048
drwxr-xr-x 4 jormin staff 128B 9 10 09:54 database
-rwxr-xr-x 1 jormin staff 18M 9 20 19:21 gin
-rw-r--r-- 1 jormin staff 217B 9 10 09:43 go.mod
-rw-r--r-- 1 jormin staff 13K 9 10 09:43 go.sum
-rw-r--r-- 1 jormin staff 330B 9 9 19:22 main.go
drwxr-xr-x 4 jormin staff 128B 9 9 19:19 modules
drwxr-xr-x 5 jormin staff 160B 9 11 14:50 routers
drwxr-xr-x 8 jormin staff 256B 9 10 09:43 vendor
➜ gin go build
➜ gin ll
total 36048
drwxr-xr-x 4 jormin staff 128B 9 10 09:54 database
-rwxr-xr-x 1 jormin staff 18M 9 20 19:21 gin
-rw-r--r-- 1 jormin staff 217B 9 10 09:43 go.mod
-rw-r--r-- 1 jormin staff 13K 9 10 09:43 go.sum
-rw-r--r-- 1 jormin staff 330B 9 9 19:22 main.go
drwxr-xr-x 4 jormin staff 128B 9 9 19:19 modules
drwxr-xr-x 5 jormin staff 160B 9 11 14:50 routers
drwxr-xr-x 8 jormin staff 256B 9 10 09:43 vendor以代码包的导入路径作为参数时,该代码包及其依赖会被编译。
加入 -a 标记后所有涉及到的代码包都会被重新编译
不加入 -a 标记,则只会编译归档文件不是最新的代码包
以若干源码文件作为参数,只有这些文件会被编译,如果列出的文件不全,可能会产生编译错误。
3.3 go install
用于编译并安装代码包或源码文件
安装代码包会在
当前工作区/pkg/平台相关目录
下生成归档文件安装命令源码文件会在
当前工作区/bin
或$GOBIN
目录下生成可执行文件。- 不追加任何参数时,会试图把当前目录作为代码包并安装。
以代码包的导入路径作为参数时,该代码包及其依赖会被安装。
以命令源码文件及相关源码文件作为参数时,只有这些问价会被编译并安装。
1 | ➜ gin ll ~/go/bin |
3.4 go get
从远程代码仓库上下载并安装代码包,受支持的代码版本控制系统有:Git、Mercurial(hg)、SVN、Bazaar。
指定的代码包会被下载到 $GOPATH/src
目录中,如果开启了 mod 模式,则会下载到 $GOPATH/pkg/mod
目录中。
1 | ➜ mod go env |
-d:只执行下载动作,而不执行安装动作
-fix:在下载代码包后先执行修正动作,而后再进行编译和安装
-u:利用网路来更新已有的代码包及其依赖包
-insecure: 允许使用不安全的 HTTP 方式进行下载操作
- -x:显示相关的依赖
1 | ➜ mod go get -u -x github.com/go-errors/errors |
4.基本数据类型
4.1 程序实体与关键字
任何 Go 语言源码文件都由若干个程序实体组成,程序实体包含 变量、常量、函数、结构体和接口,它们的名字被称为 标识符
。
标识符可以是任何 Unicode 编码可以表示的字母字符、数字以及下划线”_”,不过,首字母不能是数字或下划线。
注意:Go 语言中程序实体的访问权限通过它的首字母来控制,小写表示只能被同一个包的代码访问,大写则可以被所有代码访问。
Go 语言规定来一些特定的字符序列,它们被称为 关键字
,编程人员不能把关键字作为标识符。Go 语言的关键字如下表:
用途 | 关键字 |
---|---|
程序声明 | import、package |
程序实体声明和定义 | chan、const、func、interface、map、struct、type、var |
程序流程控制 | go、select、break、case、continue、default、defer、else、fallthrough、for、goto、if、range、return、switch |
4.2 变量与常量
4.2.1 变量
Go 语言是静态类型语言,由于编译时,编译器会检查变量的类型,所以要求所有的变量都要有明确的类型。
变量分为 普通变量
和 指针变量
两种,普通变量存储的时数据本身,指针变量存放的是数据的地址。
1 | ➜ 01 cat var.go |
变量在使用前需要先声明,声明类型就约定了这个变量只能赋予该类型的值,声明一般有五种方法:
一行声明一个变量
1
var <name> <type>
使用
var
,虽然指定了类型,但是 Go 会对其进行隐式初始化,比如string
类型初始化为空字符串,int
类型初始化为0,float
为0.0,bool
为false,指针类型
为nil。声明变量也可以进行赋值,赋值时可以省略声明的类型。
如果赋值为小数,不指定类型的情况下,编译器会默认为 float64,一般不需要这么大精度,所以最好手动设定为 float32
多个变量一起声明
1
2
3
4
5
6var (
<name> <type>
<name> <type>
<name> <type>
...
)声明和初始化一个变量
使用
:=
可以声明并显式初始化一个变量,编译器会自动根据右侧值类型推断左侧对应的类型,但这种方法只能用于函数内部。1
2
3
4
5name := "hello world"
// 等价于
var name string = "hello world"
// 等价于
var name = "hello world"声明和初始化多个变量
1
name, age := "周杰伦", 36
这种方法经常用于变量的交换,如下:
1
2
3var a int = 100
var b int = 200
a, b = b, anew 函数声明一个指针变量
new 函数是 Go 语言的一个内建函数,本质类似于一种语法糖,而不是一个新的基础概念,使用表达式
new(Type)
将创建一个 Type 类型的匿名变量,初始化 Type 类型的零值,返回变量的地址,返回的指针类型为*Type
。1
2
3
4
5
6
7
8
9
10
11
12
13➜ 01 cat new.go
package main
import "fmt"
func main () {
ptr := new(int)
fmt.Println("指针变量的内存地址:", ptr)
fmt.Println("指针变量存储的值", *ptr)
}
➜ 01 go run new.go
指针变量的内存地址: 0xc00001a0b0
指针变量存储的值 0用 new 创建变量和普通变量声明语句没有区别,仅仅是不需要设置一个变量名字,在表达式中也可以使用
new(Type)
。1
2
3
4
5
6
7
8func newInt() *int {
return new(int)
}
// 等价于
func newInt() *int {
var a = 10
return &a
}
无论使用以上哪种方式,变量都只能声明一次,多次声明编译器会报错,除了匿名变量,匿名变量也成为占位符,或者空白标识符,用下划线 _
表示。
匿名变量有三个优点:
- 不分配内存,不占用内存空间
- 不需要纠结无用的变量名
- 多次声明不会有任何问题
4.2.2 常量
常量和变量的区别在于:
- 常量只能被赋予基本数据类型的值本身
- 常量不能出现只声明不赋值的情况
常量常见的声明方式:
一行声明一个常量
1
const <name> <type> = <value>
多个常量一起声明
1
2
3
4
5
6const {
<name> <string> = <value>
<name> <string> = <value>
<name> <string> = <value>
...
}
代码示例:
1 | ➜ 01 cat const.go |
4.3 整数类型
4.3.1 命名和宽度
Go 语言的整数类型一共有10个,计算架构相关的有两个:有符号整数类型 int
、无符号整数类型 uint
。
有符号整数类型会使用最高位比特表示正负,因而会使表达的整数范围变小,而无符号会用所有比特位表示数值,如此类型的值均为整数。
10中类型的宽度(比特)如下:
数据类型 | 符号 | 类型宽度(比特/byte) |
---|---|---|
int | 有 | 32/64 |
int8 | 有 | 8 |
int16 | 有 | 16 |
int32 | 有 | 32 |
int64 | 有 | 64 |
uint | 无 | 32/64 |
uint8 | 无 | 8 |
uint16 | 无 | 16 |
uint32 | 无 | 32 |
uint64 | 无 | 64 |
int 和 unit 类型的特殊之处在于不同计算架构的计算机上,它们体现的宽度是不同的,如下:
数据类型 | 计算架构 | 类型宽度(比特/byte) | 类型宽度(字节/bit) |
---|---|---|---|
int | 32位 | 32 | 4 |
int | 64位 | 64 | 8 |
uint | 32位 | 32 | 4 |
uint | 64位 | 64 | 8 |
不同整数类型宽度对应的数值范围:
类型宽度(比特/byte) | 数值范围(有符号) | 数值范围(无符号) |
---|---|---|
8 | -128 - 127 | 0 -255 |
16 | -32768 - 32767 | 0 - 65535 |
32 | 约-21.47亿 - 21.47亿 | 0 - 约42.94亿 |
64 | 约-922亿亿 - 922亿亿 | 0 - 约1844亿亿 |
4.3.2 不同进制的表示方法
计算机中,整数的进制表示方法有:2进制、8进制、10进制以及16进制,其中10进制是常用的表示访问,因为它最直观,而在计算机中整数则必是2进制存储,下面为整数10在不同进制的表示方法:
1 | ➜ 01 cat int.go |
4.4 浮点数类型
浮点数类型有两个:float32 和 float64,存储空间分别为 4个字节(32比特) 和 8个字节(64比特)。
浮点数类型的值一般由 整数部分
、小数点.
及 小数部分
组成,其中,整数部分和小数部分均由 10进制
表示,也可以加入指数部分,指数部分由 E
或 e
以及一个带正负号的10进制数组成,比如 3.7E-2
代表的是 0.037
。
Go语言中,浮点数的相关部分只能由10进制表示,而不能用8进制或者16进制表示。
float32 和 float64 的队别如下:
类型 | 最小值 | 最大值 | 精度 |
---|---|---|---|
float32 | 1.4e-45 | 3.4e38 | 小数点后6位 |
float64 | 4.9e-324 | 1.8e308 | 小数点后15位 |
4.5 复数类型
复数类型由两个:complex64 和 complex128,存储空间分别为 8个字节(64比特) 和 16个字节(128比特)。
复数类型一般由 浮点数表示的实数部分
、加号+
、 浮点数表示的序数部分
及 小写字母i
组成,如 3.7E1 + 5.89E-2i
。正由于复数由两个浮点数组成,因而附属的规则遵从浮点数的规则。
4.6 byte 与 rune
byte 与 rune 都是 别名类型,byte 是 unit8 的别名,rune 是 int32 的别名,产生这两个别名的原因是 uint8 和 int32 直观上看是数值,但实际上可以代表字符串,为了消除这两种直观错觉,就诞生了 byte 和 rune。
byte 类型需要用8个字节表示,其表示法和 int8 一致。
rune 类型的值可表示一个 Unicode 字符,需要用单引号 '
包裹,除此之外还有集中表示形式,如下图:
另外,rune 类型值的表示中支持几种特殊的字符序列:转义符,它们由 \
和一个英文字符组成,如下图:
4.7 字符串类型
一个字符串的值可以代表一个字符序列,实际在底层,一个字符串值却是由若干个字节来表现和存储的,一个字符串会被 Go 语言用 Unicode 编码规范中国的 UTF-8 编码格式编码为字节数组。
注意:
- 一个字符串值使用
len
函数得到代表它的字节数组的长度,这与表象是不同的。 - 字符串的值是不可变的,一旦创建了一个此类型的值,就不可能对它本身做任何修改。
字符串有两种表示法:
- 原声表示法:用反引号` 包裹字符串,表示所见即所得
- 解释性表示法:用双引号 “ 包裹字符串,表示值中的转义符会起作用,因而实际长度与表象不同。
5. 高级数据类型
5.1 数组 - Array
一个数组就是一个可以容纳若干相同类型元素的容器,容器大小(数组长度)是固定的,体现在数组的类型字面量之中,由于数组长度是固定的,因而在 Go 语言中很少直接使用数组。
类型声明语句由 关键字(type)
、类型名称
、类型字面量
组成,如下声明了一个数组类型:
1 | type numbers [3]int |
类型字面量用于表示某个类型的字面表示(或标记方法),相对的,用于表示某个类型的值的字面表示可被称为值字面量,或简称为字面量。
声明初始化数组的方式:
先声明再依据索引赋值
1
2
3
4var arr [3]int
arr[0] = 0
arr[1] = 1
arr[2] = 2声明并直接初始化
使用
...
可以免去数元素个数的工作1
2
3
4
5var arr [3]int = [3]int{0, 1, 2}
// 等价于
arr := [3]int{0, 1, 2}
// 等价于
arr := [...]int{0, 1, 2}
注意:索引表达式由字符串、数组、切片或字典类型的值(或者代表词类型的变量或常量)和由方括号包裹的索引值组成。
如果只声明数组类型的变量但不赋值,默认情况下该变量长度将会是指定类型的长度,每个元素的值为指定类型的零值。
1 | // 只声明变量不赋值,默认情况下 arr 的值会是 [3]int{0, 0, 0} |
数组还有一种偷懒的定义方式,如下:
1 | // 4 表示数组有三个元素,3 表示前面三哥元素为零值,1 表示最后一个元素为 1 |
示例题:
1 | var numbers2 [5]int |
5.2 切片 - Slice
切片与数组一样,可以容纳若干相同类型的元素,不同的是,无法通过切片类型确定其长度。每个切片值都会将数组作为其底层结构,这样的数组称为切片数组。
切片类型的声明示例:
1 | // 切片类型 slice1 是 []int 的别名类型 |
切片的构造方式:
对数组进行片段截取
使用这种方式生成切片对象时,切片的容量会从截取的起始索引到原数组的终止索引。
中括号中可以设定三个参数:下界索引、上界索引[不含]、容量上界索引[不含],切出来的数据中包含下界索引,但不会包含上界索引。
1
2
3
4
5
6
7
8
9
10arr8 := [...]int{1, 2, 3, 4, 5, 6, 7, 8}
slice1 := arr8[1:5]
fmt.Printf("slice1:%d 的类型是 %T\n", slice1, slice1)
fmt.Printf("arr8 的长度是:%d,容量是:%d\n", len(arr8), cap(arr8))
fmt.Printf("slice1 的长度是:%d,容量是:%d\n", len(slice1), cap(slice1))
// 执行结果为
slice1:[2 3 4 5] 的类型是 []int
arr8 的长度是:8,容量是:8
slice1 的长度是:4,容量是:7通过数组截取的时候可以增加第三个参数来设定 容量上界索引,它的意义在于可以把作为结果的切片值的容量设置的更小,即它可以限制通过该切片值访问底层数组的元素。
因为切片的底层是数组,因而通过数组截断得到的切片可以通过延展长度访问底层数组的更多元素,这个有安全隐患,通过设定 容量上界索引 可以解决这个问题。
注意,一旦扩展操作超出了被操作的切片值的容量,那么该切片的底层数组就会被自动更换。这也使得通过设定容量上界索引来对其底层数组进行访问控制的方法更加严谨了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47➜ study cat 01/slice.go
package main
import "fmt"
func main() {
// 定义一个数组
arr := [5]int{0, 1, 2, 3, 4}
// 定义一个切片
slice := arr[1:3]
fmt.Println("arr", arr)
fmt.Printf("arr 的 长度为 %d,容量为 %d\n", len(arr), cap(arr))
fmt.Println("slice", slice)
fmt.Printf("slice 的 长度为 %d,容量为 %d\n", len(slice), cap(slice))
// 此时看起来一起都正常,但需要注意,切片的底层是数组,因而可以使用如下操作访问底层数组的更多元素
slice = slice[:cap(slice)]
fmt.Println("延长 slice 长度后")
fmt.Println("slice", slice)
fmt.Printf("slice 的 长度为 %d,容量为 %d\n", len(slice), cap(slice))
// 可以看到延长 slice 的长度后, slice 的长度和容量已经相同,都为4,并且最后一个元素的值为底层数组的元素
// 这就意味着可以通过数组截取的 slice 访问底层数组的更多元素,这是一个安全隐患
// 此时我们可以给切片增加第三个参数来限制数组的容量上界索引,此时访问超过上界索引的时候就会抛出异常
slice = arr[1:3:3]
slice = slice[:cap(slice)]
fmt.Println("限定 slice 上界索引后")
fmt.Println("slice", slice)
fmt.Printf("slice 的 长度为 %d,容量为 %d\n", len(slice), cap(slice))
}
// 执行结果
➜ study go run 01/slice.go
arr [0 1 2 3 4]
arr 的 长度为 5,容量为 5
slice [1 2]
slice 的 长度为 2,容量为 4
延长 slice 长度后
slice [1 2 3 4]
slice 的 长度为 4,容量为 4
限定 slice 上界索引后
slice [1 2]
slice 的 长度为 2,容量为 2从头声明赋值
1
2
3
4
5
6
7
8
9
10
11
12
13// 声明字符串切片
var slice1 []string
// 声明整型切片
var slice2 []int
// 声明一个空切片
var slice3 = []int{}
fmt.Println(slice1)
fmt.Println(slice1 == nil)
// 执行结果为
[]
true使用 make 函数构造
make 函数的格式为
make([]Type, Size, Cap)
,三个参数分别为:类型、长度、容量1
2
3
4
5
6
7
8
9
10var slice2 []string = make([]string, 2, 5)
var slice3 []int = make([]int, 2, 5)
fmt.Println(slice2, slice3)
fmt.Println(len(slice2), cap(slice2))
fmt.Println(len(slice3), cap(slice3))
// 执行结果为
[ ] [0 0]
2 5
2 5使用和数组一样的偷懒方式
1
2
3
4
5
6
7
8var slice5 = []int{4: 2}
fmt.Println(slice5)
fmt.Println(len(slice5), cap(slice5))
// 执行结果为
[0 0 0 0 2]
5 5复制切片
使用
copy
函数来复制切片,该函数接受两个相同类型切片参数,把第二个切片的元素复制到第一个切片的相应位置上,这里需要注意:- 遵循最小复制原则:被复制的元素的个数总是等于长度较短的那个参数的长度 - 与 append 不同,copy 函数会直接对第一个切片的值进行修改
1
2
3
4
5
6
7
8
9
10
11
12
13
14slice1 := []int{1, 2, 3}
slice2 := []int{0, 0, 0, 0, 0}
fmt.Println("复制前")
fmt.Println(slice1, slice2)
copy(slice2, slice1)
fmt.Println("复制后")
fmt.Println(slice1, slice2)
// 执行结果
复制前
[1 2 3] [0 0 0 0 0]
复制后
[1 2 3] [1 2 3 0 0]
切片不同与数组的地方:
数组的长度固定,而切片的容器大小不固定,切片本身是引用类型(零值为nil),可以对它进行 append 操作来添加元素,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20slice6 := []int{1}
fmt.Println(slice6)
slice6 = append(slice6, 2, 3, 4)
fmt.Println(slice6)
slice6 = append(slice6, []int{7, 8}...)
fmt.Println(slice6)
slice6 = append([]int{0}, slice6...)
fmt.Println(slice6)
fmt.Println(slice6[:5])
slice6 = append(slice6[:5], append([]int{5, 6}, slice6...)...)
fmt.Println(slice6)
// 执行结果为
[1]
[1 2 3 4]
[1 2 3 4 7 8]
[0 1 2 3 4 7 8]
[0 1 2 3 4]
[0 1 2 3 4 5 6 0 1 2 3 4 7 8]数组的长度与容量相同,而切片则往往不同,如下图:
可以使用
len
函数获取切片和数组长度,用cap
函数获取切片和数组容量。
完整的截取数组、延展长度、append 和 copy 操作示例如下:
1 | var numbers4 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} |
5.3 字典 - Map
字典其实是哈希表(Hash Table)的一个实现,用于存储键值对的无序集合,其中,每一个 key 都是唯一的,如果放入一个已经存在的 key 时,会进行覆盖操作。
字典类型的字面量为 map[K]T
,K 代表类型,T 代表值,需要注意,字典的key类型必须是可比较的,否则会引起错误,也就是说,它不能是切片、字典或函数类型。
声明一个字典变量后,默认情况下,每一个元素的值都为定义的类型的空值。
字典与切片一样都是引用类型,它的零值为nil。
字典的声明方法:
1 | // 第一种 |
字典的相关操作:
添加元素
1
map1["d"] = 4
更新元素
1
map1["d"] = 5
读取元素,当访问不存在的 key 时,并不会报错,而是会返回指定值类型的空值
1
fmt.Println(map1["d"])
删除元素,无论删除的元素在不在都会无声地执行完毕,即 有则删除,无则不处理。
1
delete(map1, "d")
判断 key 是否存在,根据返回的第二个数据进行判断,为true则存在
1
val, isExist = map1["d"]
循环
1
2
3
4
5
6
7
8
9
10
11
12// 同时获取 key 和 val
for key, val := range map1 {
fmt.Printf("key:%s,val:%d\n", key, val)
}
// 只获取 key 可以不用定义第二个变量
for key := range map1 {
fmt.Printf("key:%s\n", key)
}
// 只获取 val,则第一个参数需要用占位符
for _, val := range map1 {
fmt.Printf("val:%d\n", val)
}
完整的示例如下:
1 | ➜ study cat 01/map.go |
5.4 通道 - Channel
通道是 Go 语言中一种非常特殊的数据结构,它可用于在 Goroutine 之间传递类型化的数据,并且是并发安全的,相比之下,上面介绍的数据类型都不是并发安全的。
如果说 Goroutine 是 Go 语言程序的并发体,那么通道就是它们之间的通信机制。通道就是一个管道,连接多个 Goroutine 程序,它是一种队列式的数据结构,遵循先入先出的规则。
通道类型和切片及字典一样,都是引用类型,空值都为 nil
。
通道类型的表示方法很简单,仅由两部分组成:chan T
,与其他数据类型不同的是,无法表示一个通道类型的值,因此也无法用字面量为通道类型的变量赋值,只能通过 make
函数来达到目的,make 函数的第一个参数是类型的字面量,如 chan int
,第二个参数是值的长度,示例如下:
1 | cha1 := make(chan int, 5) |
确切的说,通道值的长度代表通道中可以暂存的数据个数。
5.4.1 定义与使用
每个通道都 只能传递一种数据类型 的数据,所以在声明的时候需要指定数据类型,声明后的通道,零值为 nil
,无法直接使用,需要配合 make
函数使用。
1 | // 声明 |
5.4.2 长度与容量
- 当容量为 0 时,表明通道不能存放数据,发送数据时,要求必须立即接收,否则会报错,此时的通道称之为 无缓冲通道 。
- 当容量为 1 时,表明通道只能缓存一个数据,若通道已有1个数据,再次发送数据会阻塞,利用这点可以使用通道做 锁 。
- 当容量大于 1 时,通道中可以存放多个数据,可以用于多个 Goroutine 之间的通信及共享资源
5.4.3 常用操作
发送数据:使用
<-
1
cha1 <- "value1"
接收数据:使用
<-
1
val, status := <- cha1
返回状态的值的作用是消除零值的歧义,因为通道关闭后也会返回零值,加入状态字段,通过判断 true|false 就可以准确的处理通道数据。
关闭通道:使用
close
方法1
close(cha1)
遍历通道:使用
for
和range
,遍历时一定要确定通道是否处于关闭状态,否则会阻塞。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50➜ study cat 01/channel1.go
package main
import (
"fmt"
)
func main() {
pipline := make(chan int, 10)
// 使用协程往通道发送数据
go sendData(pipline)
// 遍历通道数据
for val := range pipline {
fmt.Printf("[Main] 接收到数据:%d\n", val)
}
}
func sendData(pipline chan int) {
for i:=0; i< cap(pipline); i++ {
fmt.Printf("[Send] 发送数据:%d\n", i)
pipline <- i
}
// 发送完毕后记得关闭通道,否则会阻塞
close(pipline)
fmt.Println("[Send] 已关闭通道")
}
➜ study go run 01/channel1.go
[Send] 发送数据:0
[Send] 发送数据:1
[Send] 发送数据:2
[Send] 发送数据:3
[Send] 发送数据:4
[Send] 发送数据:5
[Send] 发送数据:6
[Send] 发送数据:7
[Send] 发送数据:8
[Send] 发送数据:9
[Send] 已关闭通道
[Main] 接收到数据:0
[Main] 接收到数据:1
[Main] 接收到数据:2
[Main] 接收到数据:3
[Main] 接收到数据:4
[Main] 接收到数据:5
[Main] 接收到数据:6
[Main] 接收到数据:7
[Main] 接收到数据:8
[Main] 接收到数据:9
需要注意几点:
- 针对通道的发送操作会在通道已满时阻塞。
- 针对通道的接收操作会在通道已空时阻塞。
- 重复关闭通道会抛异常。
- 关闭尚未初始化的通道会抛异常。
- 针对已关闭的通道发送数据会抛异常。
- 针对已关闭的通道接收数据不会抛异常,如果还有数据会被读取到,否则会得到零值,需要根据第二个返回字段判断通道状态。
- 关闭通道会产生一个广播机制,所有向通道读取消息的 Goroutine 都会收到消息。
- 通道在 Go 语言中地位很高,它是线程安全的,面对并发问题,应当首先想到通道。
5.4.4 分类
按照是否可以缓冲数据可以分为:
缓冲通道:允许通道存储 1个或多个 数据,这意味着 发送端和接收端可以是异步状态 。
1
pipline := make(chan int, 10)
无缓冲通道:无法存储数据,意味着 接收端必须先于发送端准备好,以确保发送完数据后可以立即接收数据 ,否则发送端会阻塞,也就是说,发送端和接收端是同步状态 。
1
2
3
4
5pipline := make(chan int, 0)
// 等价于
pipline := make(chan int)
按照通道的数据流向可以分为:
双向通道:一般定义的通道都是双向通道,可以发送数据也可以接收数据
单向通道:分为 只读通道(type Receiver) 和 只写通道(type Sender)
如上,可以使用别名来定义单向通道,它们的主要区别在于
<-
在关键字chan
的左边还是右边<- chan
:表示这个通道只能发送数据,即只读通道chan<-
:表示这个通道只能接收数据,即只写通道
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21➜ study cat 01/channel.go
package main
import "fmt"
func main() {
var pipline1 = make(chan int, 10)
type Receiver = <-chan int
var receiver Receiver = pipline1
fmt.Println(receiver)
var pipline2 = make(chan int, 10)
type Sender = chan<- int
var sender Sender = pipline2
fmt.Println(sender)
}
➜ study go run 01/channel.go
0xc000104000
0xc0001040b0从上文的例子可以发现,定义单向通道之前都先声明了一个双向通道,之后再声明单向通道变量并赋值,这样做的原因在于:通道本身是为了传输数据,如果只入不出 或者 只出不入 就一点意义都没有了,所以这两类通道唇亡齿寒,缺一不可。
当然,如果往只读通道写入数据,或者从只写通道读取数据,都会报错。
完整示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44➜ study cat 01/channel.go
package main
import (
"fmt"
"time"
)
// 声明一个双向通道
var pipline = make(chan int, 10)
// 定义只读通道
type Receiver = <-chan int
// 定义只写通道
type Sender = chan<- int
func main() {
// 声明只写通道,并发送数据
go func() {
var sender Sender = pipline
fmt.Println("[Sender] 开始发送数据:100")
sender <- 100
fmt.Println("[Sender] 发送数据完毕")
}()
// 声明只读通道,并接收数据
go func() {
var receiver Receiver = pipline
fmt.Println("[Receiver] 准备接收数据")
val := <- receiver
fmt.Printf("[Receiver] 接收到的数据为:%d\n", val)
}()
// 主函数 sleep,保证上面两个协程都可以执行完毕
time.Sleep(time.Second)
}
➜ study go run 01/channel.go
[Sender] 开始发送数据:100
[Sender] 发送数据完毕
[Receiver] 准备接收数据
[Receiver] 接收到的数据为:100
使用通道做锁,如下所示,如果不加锁,循环 1000 次后,最后打印的结果会小于 1000,而加了锁则不会。
1 | ➜ study cat 01/channel2.go |
###
本文作者:Jormin
本文地址: https://blog.lerzen.com/go学习笔记/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!