【读书笔记&个人心得】第9章:包
标准库概述
标准库的完整列表通过https://gowalker.org/search?q=gorepos查看
unsafe:
包含了一些打破 Go 语言“类型安全”的命令,一般的程序中不会被使用,可用在 C/C++ 程序的调用中
syscall-os-os/exec
os: 提供给我们一个平台无关性的操作系统功能接口,采用类 Unix 设计,隐藏了不同操作系统间的差异,让不同的文件系统和操作系统对象表现一致。
os/exec: 提供我们运行外部操作系统命令和程序的方式。
syscall: 底层的外部包,提供了操作系统底层调用的基本接口。
archive/tar 和 /zip-compress
压缩(解压缩)文件功能
archive(档案)compress(压缩)
fmt-io-bufio-path/filepath-flag
fmt: 提供了格式化输入输出功能。
io: 提供了基本输入输出功能,大多数是围绕系统功能的封装。
bufio: 缓冲输入输出功能的封装。
path/filepath: 用来操作在当前系统中的目标文件名路径。
flag: 对命令行参数的操作。
strings-strconv-unicode-regexp-bytes
strings: 提供对字符串的操作。
strconv: 提供将字符串转换为基础类型的功能。(字符串和基础类型互转)
unicode: 为 unicode 型的字符串提供特殊的功能。
regexp: 正则表达式功能。
bytes: 提供对字符型分片的操作。(即对byte[]的操作)
index/suffixarray: 子字符串快速查询。
suffix(后缀)
math-math/cmath-math/big-math/rand-sort
math: 基本的数学函数。
math/cmath: 对复数的操作。(复数的集合用C表示,实数的集合用R表示)
math/rand: 伪随机数生成。
sort: 为数组排序和自定义集合。(// TODO:自定义集合?)
math/big: 大数的实现和计算。
container-/list-ring-heap
实现对集合的操作
list: 双链表。
ring: 环形链表。
下面代码演示了如何遍历一个链表(当 l 是 *List)
for e := l.Front(); e != nil; e = e.Next() {
//do something with e.Value
}
time-log
ime: 日期和时间的基本操作。
log: 记录程序运行时产生的日志,我们将在后面的章节使用它。
encoding/json-encoding/xml-text/template
encoding/json: 读取并解码和写入并编码 JSON 数据。
encoding/xml: 简单的 XML1.0 解析器,有关 JSON 和 XML 的实例请查阅第 12.9/10 章节。
text/template:生成像 HTML 一样的数据与文本混合的数据驱动模板(参见第 15.7 节)。
net-net/http-html:(参见第 15 章)
net: 网络数据的基本操作。
http: 提供了一个可扩展的 HTTP 服务器和基础客户端,解析 HTTP 请求和回复。
html: HTML5 解析器。
runtime
Go 程序运行时的交互操作,例如垃圾回收和协程创建。
reflect
实现通过程序运行时反射,让程序操作任意类型的变量
exp
exp 包中有许多将被编译为新包的实验性的包。在下次稳定版本发布的时候,它们将成为独立的包。如果前一个版本已经存在了,它们将被作为过时的包被回收。然而 Go1.0 发布的时候并没有包含过时或者实验性的包
练习
使用 container/list 包实现一个双向链表,将 101、102 和 103 放入其中并打印出来。
参考资料:https://cloud.tencent.com/developer/section/1140666
package main
import (
"container/list"
"fmt"
)
func main() {
// 创建一个新列表并在其中添加一些数字。
l := list.New()
e1 := l.PushBack(1)
fmt.Printf("刚添加的元素的结构体:%d\n", e1)
e2 := l.PushFront(101)
fmt.Printf("刚添加的元素的结构体:%d\n", e2)
e3 := l.InsertBefore(102, e1)
fmt.Printf("刚添加的元素的结构体:%d\n", e3)
e4 := l.InsertAfter(103, e1)
fmt.Printf("刚添加的元素的结构体:%d\n", e4)
// 遍历列表并打印其内容。
for e := l.Front(); e != nil; e = e.Next() {
fmt.Println(e.Value)
}
}
刚添加的元素的结构体:&{824633794992 824633794992 824633794992 1}
刚添加的元素的结构体:&{824633795040 824633794992 824633794992 101}
刚添加的元素的结构体:&{824633795040 824633795136 824633794992 102}
刚添加的元素的结构体:&{824633794992 824633795040 824633794992 103}
101
102
1
103
元素的声明结构
{
next, prev *Element
list *List
Value any
}
通过使用 unsafe 包中的方法来测试你电脑上一个整型变量占用多少个字节。
package main
import (
"fmt"
"unsafe"
)
func main() {
var a int
fmt.Printf("%d", unsafe.Sizeof(a))
}
regexp 包
正则表达式语法和使用的详细信息请参考 维基百科
简单模式,使用 Match() / MatchString() 方法便可
package main
import (
"fmt"
"regexp"
)
func main() {
str, str2 := "0123456789abc", "abc"
ok, _ := regexp.Match("[0-9]", []byte(str))
ok2, _ := regexp.Match("[0-9]", []byte(str2))
ok3, _ := regexp.MatchString("[0-9]", str)
fmt.Println(ok, ok2, ok3)
}
复杂模式
先将正则模式通过 Compile() 方法返回一个 Regexp 对象
package main
import (
"fmt"
"regexp"
"strconv"
)
func main() {
//目标字符串
searchIn := "John: 2578.34 William: 4567.23 Steve: 5632.18"
pat := "[0-9]+.[0-9]+" //正则:查找一个浮点数
f := func(s string) string { //字符串转浮点数,可能会转换失败
v, _ := strconv.ParseFloat(s, 32)
return strconv.FormatFloat(v*2, 'f', 2, 32)
}
if ok, _ := regexp.Match(pat, []byte(searchIn)); ok {
fmt.Println("Match Found!")
}
re, _ := regexp.Compile(pat)
//将匹配到的部分替换为"##.#"
str := re.ReplaceAllString(searchIn, "##.#") // 在pat规则下做操作
fmt.Println(str)
//参数为函数时
str2 := re.ReplaceAllStringFunc(searchIn, f)
fmt.Println(str2)
}
带报错的Compile()
Compile() 函数也可能返回一个错误,我们在使用时忽略对错误的判断是因为我们确信自己正则表达式是有效的。当用户输入或从数据中获取正则表达式的时候,我们有必要去检验它的正确性。另外我们也可以使用 MustCompile() 方法,它可以像 Compile() 方法一样检验正则的有效性,但是当正则不合法时程序将 panic()(详情查看第 13.2 节)
锁和 sync 包
问题
在一些复杂的程序中,通常通过不同线程执行不同应用来实现程序的并发。当不同线程要使用同一个变量时,经常会出现一个问题:无法预知变量被不同线程修改的顺序!(这通常被称为资源竞争,指不同线程对同一变量使用的竞争)显然这无法让人容忍,那我们该如何解决这个问题呢?
解决办法:加锁
经典的做法是一次只能让一个线程对共享变量进行操作。当变量被一个线程改变时(临界区),我们为它上锁,直到这个线程执行完成并解锁后,其他线程才能访问它。
map没有锁机制,所以 map 类型是非线程安全的。当并行访问一个共享的 map 类型的数据,map 数据将会出错
在 Go 语言中这种锁的机制是通过 sync 包中 Mutex 来实现的。sync 来源于 "synchronized" 一词,这意味着线程将有序的对同一变量进行访问。
sync.Mutex 是一个互斥锁,它的作用是守护在临界区入口来确保同一时间只能有一个线程进入临界区
相对简单的情况下,通过使用 sync 包可以解决同一时间只能一个线程访问变量或 map 类型数据的问题。如果这种方式导致程序明显变慢或者引起其他问题,我们要重新思考来通过 goroutines 和 channels 来解决问题,这是在 Go 语言中所提倡用来实现并发的技术。我们将在第 14 章对其深入了解,并在第 14.7 节中对这两种方式进行比较。
其实互斥锁使用很简单,就是手动加锁
package main
import (
"fmt"
"sync"
)
type Info struct {
mu sync.Mutex
Str string
}
func main() {
var info Info
Update(&info)
}
func Update(info *Info) {
info.mu.Lock()
info.Str = "abc"
info.mu.Unlock()
fmt.Printf("%s", info.Str)
}
共享缓冲器
还有一个很有用的例子是通过 Mutex 来实现一个可以上锁的共享缓冲器:
type SyncedBuffer struct {
lock sync.Mutex
buffer bytes.Buffer
}
RWMutex 锁:单写多读
如果使用 Lock() 将和普通的 Mutex 作用相同
sync.RLock()
sync.RUnlock()
一次性函数
sync包中还有一个方便的 Once 类型变量的方法 once.Do(call),这个方法确保被调用函数只能被调用一次。
精密计算和 big 包
我们知道有些时候通过编程的方式去进行计算是不精确的
浮点计算,Go最高精确度是 float64 ,结果将精确到 小数点后 15 位(我的试验是点后16位),足以满足大多数的任务
package main
import "fmt"
func main() {
var f64 float64
f64 = 1 + 2.5/3
fmt.Println(f64)
}
输出
1.8333333333333333
浮点数的精度是有限的
当对超出 int64 或者 uint64 (uint64表示的正数范围比int64大)类型这样的大数进行计算时,如果对精度没有要求,float32 或者 float64 可以胜任,但如果对精度有严格要求的时候,我们不能使用浮点数,在内存中它们只能被近似的表示
只要内存足够大
对于整数的高精度计算 Go 语言中提供了 big 包,被包含在 math 包下:有用来表示大整数的 big.Int 和表示大有理数的 big.Rat 类型(可以表示为 2/5 或 3.1416 这样的分数,而不是无理数或 π)。
这些类型(big.Int、big.Rat)可以实现任意位类型的数字,只要内存足够大。缺点是更大的内存和处理开销使它们使用起来要比内置的数字类型慢很多(我的理解,因为64位机一次最大吞吐处理64位,也就是8个字节,64以内可以一次性处理,而big超过64只能分几次处理,时间就长了)
PS:大数运算,顾名思义,就是对很大的数进行一系列的运算。在数学中,数值的大小是没有上限的,但是在计算机中,由于字长的限制,计算机所能表示的范围有限,对于很大的数,计算机无法对其进行直接计算,需要用到所谓的高精度算法,即用数组来存储整数,并模拟手算的方法进行四则运算。
参考资料:https://zhuanlan.zhihu.com/p/142133669
构造和使用
大的整型数字是通过 big.NewInt(n) 来构造的,其中 n 为 int64 类型整数。而大有理数是通过 big.NewRat(n, d) 方法构造。n(分子)和 d(分母)都是 int64 型整数。因为 Go 语言不支持运算符重载(也就是无法修改+-*/使其接受big这个异类),所以所有大数字类型都有像是 Add() 和 Mul() 这样的方法。
节省内存,不使用临时变量
Add() 和 Mul() 这样的方法,它们作用于作为 receiver 的整数和有理数,大多数情况下它们修改 receiver 并以 receiver 作为返回结果。因为没有必要创建 big.Int 类型的临时变量来存放中间结果,所以运算可以被链式地调用,并节省内存。
PS:关于receiver
在go语言中,没有类的概念但是可以给类型(结构体,自定义类型)定义方法。所谓方法就是定义了接受者的函数
参考资料:https://www.jianshu.com/p/316617954070
示例
加减乘除的英文:Add、subtract、multiply 、divide
// big.go
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)
}
/* Output:
Big Int: 43492122561469640008497075573153004
Big Rat: -37/112
*/
自定义包和可见性
包是 Go 语言中代码组织和代码编译的主要方式
当写自己包的时候,要使用短小的不含有 _(下划线)的小写单词来为文件命名。这里有个简单例子来说明包是如何相互调用以及可见性是如何实现的
examples/chapter_9/book/package_mytest.go
package main
import (
"fmt"
"./pack1"// import "包的路径或 URL 地址",如import "github.com/org1/pack1"
)
func main() {
var test1 string
test1 = pack1.ReturnStr()
fmt.Printf("ReturnStr from package1: %s\n", test1)
fmt.Printf("Integer from package1: %d\n", pack1.Pack1Int)
// fmt.Printf("Float from package1: %f\n", pack1.pack1Float)
}
...../pack1/pack1.go
package pack1
var Pack1Int int = 42
var pack1Float = 3.14
func ReturnStr() string {
return "Hello main!"
}
如果包 pack1 和我们的程序在同一路径下,我们可以通过 "import ./pack1" 这样的方式来引入,但这不被视为一个好的方法
访问一个未引用的变量或者函数
fmt.Printf("Float from package1: %f\n", pack1.pack1Float)
// cannot refer to unexported name pack1.pack1Float
主程序利用的包必须在主程序编写之前被编译。主程序中每个 pack1 项目都要通过包名来使用:pack1.Item。具体使用方法请参见示例 4.6 和 4.7。
目录与包
目录与包要同名。因此,按照惯例,子目录和包之间有着密切的联系:为了区分,不同包存放在不同的目录下,每个包(所有属于这个包中的 go 文件)都存放在和包名相同的子目录下
使用 . 作为包的别名
当使用 . 作为包的别名时,你可以不通过包名来使用其中的项目。例如:test := ReturnStr()
import . "./pack1"
导入pack1包和测试
在当前的命名空间导入 pack1 包,一般是为了具有更好的测试效果
import _ "./pack1/pack1" // ./pack1/是目录名,pack1是package指定的包名
pack1 包只导入其副作用,也就是说,只执行它的 init() 函数并初始化其中的全局变量。
导入外部安装包
使用go install
作用:编译源代码
这个命令在内部实际上分成了两步操作:
第一步,生成结果文件(main包生成可执行文件,普通包生成 .a 文件)
第二步,把编译好的结果移到 $GOPATH/pkg(xxx.a) 或者 $GOPATH/bin(xxx.exe)
使用远程包
如果你要在你的应用中使用一个或多个外部包,首先你必须使用 go install(参见第 9.7 节)在你的本地机器上安装它们
这个东西主要用于安装网络上的包(远程安装包),类似npm install xxx -g
go get这个东西即将被抛弃,新版本会默认带-d即只下载不安装,还是直接使用go install
go install golang.org/x/website/tour@latest
使用本地包
go install ../ww
把当前程序编译位可执行程序
go install .
go install 和 go get 的区别(补充)
go install 被设计为“用于构建和安装二进制文件”, go get 则被设计为 “用于编辑 go.mod 变更依赖”
参考资料:
https://www.csdn.net/tags/MtjaQgysNzY5NTgtYmxvZwO0O0OO0O0O.html
https://golang.google.cn/doc/modules/managing-dependencies
安装并导入
通过以下方式,一次性安装,并导入到你的代码中
import goex "codesite.ext/author/goExample/goex" // TODO:待验证
包的初始化
程序的执行开始于导入包,初始化 main 包然后调用 main() 函数。
一个没有导入的(没导入过的)包将通过分配初始值给所有的包级变量(即全局变量)和调用源码中定义的包级 init() 函数来初始化。一个包可能有多个 init() 函数甚至在一个源码文件中。它们的执行是无序的。
init() 函数是不能被调用的
导入的包在包自身初始化前被初始化,而一个包在程序执行中只能初始化一次(导入的包先于自己所在的包初始化,一个包只初始化一次,即使你在其他包中再次导入?)
自己编写包并导入使用
参考资料:https://golang.google.cn/doc/tutorial/call-module-code
首先在$GOPATH/src中创建ww目录,执行go mod init,新建xx.go ,第一行写 package ww
// say_hello.go
package ww
import "fmt"
func Say() {
fmt.Println("Hello!")
}
然后,新建一个项目hello_world,执行go mod init,创建main.go
// say_hello.go
package main
import (
"ww"//导入使用自己的包
)
func main() {
ww.Say()
}
修改hello_world/go.mod,加入replace,把ww包重定向本地路径的ww
module main
go 1.18
replace ww => ../ww
命令行执行 go mod tidy,如生产如下
module main
go 1.18
replace ww => ../ww //这个目录应该有go.mod
require ww v0.0.0-00010101000000-000000000000
然后,在hello_world就可以正常使用ww包了
为自定义包使用 godoc
go doc会列出目录的所有源文件和摘取该源文件的注释
本书内容已过期,请关注官网
go module
(我觉得有了go mod,应该不太用go install了)
从Go1.13版本开始,go module将是Go语言默认的依赖管理工具
Modules是相关Go包的集合,是源代码交换和版本控制的单元。go命令直接支持使用Modules,包括记录和解析对其他模块的依赖性。Modules替换旧的基于GOPATH的方法,来指定使用哪些源文件。
层级:module-->package-->xx.Go(全局声明的范围是全包,可以跨 .go 源文件)
GO111MODULE
要启用go module支持首先要设置环境变量GO111MODULE,通过它可以开启或关闭模块支持,它有三个可选值:off、on、auto,默认值是auto
GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖,将依赖下载至%GOPATH%/pkg/mod/ 目录下。
GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,开启模块支持。
必要在GOPATH中创建项目
设置GO111MODULE=on之后就可以使用go module了,以后就没有必要在GOPATH中创建项目了,并且还能够很好的管理项目依赖的第三方包信息
使用 go module 管理依赖后会在项目根目录下生成两个文件go.mod和go.sum(文件名固定,不可改动)
参考资料:https://segmentfault.com/a/1190000022868683
包下载的源
export GOPROXY=https://goproxy.cn
go env -w GOPROXY=https://goproxy.cn,direct
Go1.13之后GOPROXY默认值为https://proxy.golang.org,在国内是无法访问的,所以十分建议大家设置GOPROXY,这里我推荐使用goproxy.cn
go mod命令
go mod download 下载依赖的module到本地cache(默认为$GOPATH/pkg/mod目录)
go mod edit 编辑go.mod文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹, 创建go.mod文件
go mod tidy 增加缺少的module,删除无用的module
go mod vendor 将依赖复制到vendor下
go mod verify 校验依赖
go mod why 解释为什么需要依赖
go.mod文件记录了项目所有的依赖信息,其结构大致如下:
module github.com/Q1mi/studygo/blogger // 自己的包名
go 1.12
require (
github.com/DeanThompson/ginpprof v0.0.0-20190408063150-3be636683586
github.com/gin-gonic/gin v1.4.0
github.com/go-sql-driver/mysql v1.4.1
github.com/jmoiron/sqlx v1.2.0
github.com/satori/go.uuid v1.2.0
google.golang.org/appengine v1.6.1 // indirect
)
module用来定义包名
require用来定义依赖包及版本
indirect表示间接引用
依赖的版本表示
go mod支持语义化版本号,比如go get foo@v1.2.3,也可以跟git的分支或tag,比如go get foo@master,当然也可以跟git提交哈希,比如go get foo@e3702bed2。关于依赖的版本支持以下几种格式:
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
gopkg.in/vmihailenco/msgpack.v2 v2.9.1
gopkg.in/yaml.v2 <=v2.2.1
github.com/tatsushid/go-fastping v0.0.0-20160109021039-d7bb493dee3e
latest
依赖的替代:replace
在国内访问golang.org/x的各个包都需要FQ,你可以在go.mod中使用replace替换成github上对应的库。
replace (
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)
下载依赖包:go get
(有了go module后,go get的作用和以前也有了不同)
在项目中执行go get命令可以下载依赖包,并且还可以指定下载的版本。
运行go get -u将会升级到最新的次要版本或者修订版本(x.y.z, z是修订版本号, y是次要版本号)
运行go get -u=patch将会升级到最新的修订版本
运行go get package@version将会升级到指定的版本号version
如果下载所有依赖可以使用go mod download命令(只下载,不安装?)
参考资料:http://c.biancheng.net/view/123.html
整理依赖
主要是整体mod文件的依赖
我们在代码中删除依赖代码后,相关的依赖库并不会在go.mod文件中自动移除。这种情况下我们可以使用go mod tidy命令更新go.mod中的依赖关系。
实际上就是根据代码使用了什么库,生成一份依赖命单写入go.mod,然后执行go get即可根据名单下载依赖。
(不止删除,添加replace后,也要使用go mod tidy整理依赖)
通过命令行控制go.mod:go mod edit
我们可以手动修改go.mod文件,所以有些时候需要格式化该文件
格式化go.mod
go mod edit -fmt
添加依赖项
go mod edit -require=golang.org/x/text
移除依赖项
如果只是想修改go.mod文件中的内容,那么可以运行go mod edit -droprequire=package path
go mod edit -droprequire=golang.org/x/text
获取更多用法:go help mod edit
项目引入go module
https://github.com/golang/tools/blob/master/gopls/doc/workspace.md
既有项目
对一个已经存在的项目启用go module,可以按照以下步骤操作
1.在项目目录下执行go mod init,生成一个go.mod文件。
2.执行go get,查找并记录当前项目的依赖,同时生成一个go.sum记录每个依赖库的版本和哈希值。
(能够自动生成依赖记录)
新项目
1.执行go mod init 项目名命令,在当前项目文件夹下创建一个go.mod文件。
2.手动编辑go.mod中的require依赖项或执行go get自动发现、维护依赖
项目(主包)和调用包的位置问题
情况一:调用包在同一module下
目录结构如下:
moduledemo(项目)
├── go.mod
├── main.go
└── mypackage
└── mypackage.go // package mp
main包所在的module的go.mod:
module moduledemo // 设定 moduledemo 为module名,可以随意改变该名字,只需要导入时一致就好
go 1.14 // 表明Go的版本
项目导入自定义包mypackage的方式为:
import "moduledemo/mypackage"
注意:
1 要加入自己的module名在最前面,然后才是包名(由自定义包首行的package指定)
2 一个目录放一个包名,包名与目录要同名,这样的好处是直接通过目录名知道包名,使用包时要通过包名引用
3 自定义包内不必再进行行go mod init
情况二:调用包不在同一module下
目录结构如下:
├── moduledemo(项目)
│ ├── go.mod
│ └── main.go
└── mypackage
├── go.mod
└── mypackage.go // package mp
moduledemo包的go.mod:
module moduledemo
go 1.14
require mypackage v0.0.0 // 这个会在你执行 go mod tidy 之后自动在该文件添加
replace mypackage => ../mypackage // 指定需要的包目录去后面这个路径中寻找
mypackage包的go.mod:
module mypackage
go 1.14
项目导入包的方式为:
import "mypackage" // 因为该包目录本身就是包文件所以无需添加下一级路径,或者说该包不存在下一层的package
mp.MyPackage() // 使用包中的 MyPackage() 函数
补充:
相对路径使用有变,假如module名为hello,main.go 中使用 internal package 的方法跟以前已经不同了,由于 go.mod 会扫描同工作目录下所有 package 并且变更引入方法,必须将 hello 当成路径的前缀,也就是需要写成 import hello/api,以往 GOPATH/dep 模式允许的 import ./api 已经失效
所以 main.go 需要改写成:module名/包名或目录名(目录名应与包名一致,和源文件名无关)
package main
import (
api "hello/api" // 这里使用的是相对路径
"github.com/labstack/echo"
)
func main() {
e := echo.New()
e.GET("/", api.HelloWorld)
e.Logger.Fatal(e.Start(":1323"))
}