Golang-包8

http://c.biancheng.net/golang/package/

Go语言包的基本概念

Go语言是使用包来组织源代码的,包(package)是多个 Go 源码的集合,是一种高级的代码复用方案。Go语言中为我们提供了很多内置包,如 fmt、os、io 等。

任何源代码文件必须属于某个包,同时源码文件的第一行有效代码必须是package pacakgeName 语句,通过该语句声明自己所在的包。

包的基本概念

Go语言的包借助了目录树的组织形式,一般包的名称就是其源文件所在目录的名称,虽然Go语言没有强制要求包名必须和其所在的目录名同名,但还是建议包名和所在目录同名,这样结构更清晰。

包可以定义在很深的目录中,包名的定义是不包括目录路径的,但是包在引用时一般使用全路径引用。比如在GOPATH/src/a/b/ 下定义一个包 c。在包 c 的源码中只需声明为package c,而不是声明为package a/b/c,但是在导入 c 包时,需要带上路径,例如import "a/b/c"

包的习惯用法:

  • 包名一般是小写的,使用一个简短且有意义的名称。
  • 包名一般要和所在的目录同名,也可以不同,包名中不能包含等特殊符号。
  • 包一般使用域名作为目录名称,这样能保证包名的唯一性,比如 GitHub 项目的包一般会放到GOPATH/src/github.com/userName/projectName 目录下。
  • 包名为 main 的包为应用程序的入口包,编译不包含 main 包的源码文件时不会得到可执行文件。
  • 一个文件夹下的所有源码文件只能属于同一个包,同样属于同一个包的源码文件不能放在多个文件夹下。

包的导入

要在代码中引用其他包的内容,需要使用 import 关键字导入使用的包。具体语法如下:

import "包的路径"

注意事项:

  • import 导入语句通常放在源码文件开头包声明语句的下面;
  • 导入的包名需要使用双引号包裹起来;
  • 包名是从GOPATH/src/ 后开始计算的,使用进行路径分隔。


包的导入有两种写法,分别是单行导入和多行导入。

单行导入

单行导入的格式如下:

import "包 1 的路径"
import "包 2 的路径"

多行导入

多行导入的格式如下:

import (
    "包 1 的路径"
    "包 2 的路径"
)

包的导入路径

包的引用路径有两种写法,分别是全路径导入和相对路径导入。

全路径导入

包的绝对路径就是GOROOT/src/GOPATH/src/后面包的存放路径,如下所示:

import "lab/test"
import "database/sql/driver"
import "database/sql"

上面代码的含义如下:

  • test 包是自定义的包,其源码位于GOPATH/src/lab/test 目录下;
  • driver 包的源码位于GOROOT/src/database/sql/driver 目录下;
  • sql 包的源码位于GOROOT/src/database/sql 目录下。

相对路径导入

相对路径只能用于导入GOPATH 下的包,标准包的导入只能使用全路径导入。

例如包 a 的所在路径是GOPATH/src/lab/a,包 b 的所在路径为GOPATH/src/lab/b,如果在包 b 中导入包 a ,则可以使用相对路径导入方式。示例如下:

// 相对路径导入
import "../a"

当然了,也可以使用上面的全路径导入,如下所示:

// 全路径导入
import "lab/a"

包的引用格式

包的引用有四种格式,下面以 fmt 包为例来分别演示一下这四种格式。

1) 标准引用格式

import "fmt"

此时可以用fmt.作为前缀来使用 fmt 包中的方法,这是常用的一种方式。

示例代码如下:

  1. package main
  2. import "fmt"
  3. func main() {
  4. fmt.Println("C语言中文网")
  5. }

2) 自定义别名引用格式

在导入包的时候,我们还可以为导入的包设置别名,如下所示:

import F "fmt"

其中 F 就是 fmt 包的别名,使用时我们可以使用F.来代替标准引用格式的fmt.来作为前缀使用 fmt 包中的方法。

示例代码如下:

  1. package main
  2. import F "fmt"
  3. func main() {
  4. F.Println("C语言中文网")
  5. }

3) 省略引用格式

import . "fmt"

这种格式相当于把 fmt 包直接合并到当前程序中,在使用 fmt 包内的方法是可以不用加前缀fmt.,直接引用。

示例代码如下:

  1. package main
  2. import . "fmt"
  3. func main() {
  4. //不需要加前缀 fmt.
  5. Println("C语言中文网")
  6. }

4) 匿名引用格式

在引用某个包时,如果只是希望执行包初始化的 init 函数,而不使用包内部的数据时,可以使用匿名引用格式,如下所示:

import _ "fmt"

匿名导入的包与其他方式导入的包一样都会被编译到可执行文件中。

使用标准格式引用包,但是代码中却没有使用包,编译器会报错。如果包中有 init 初始化函数,则通过import _ "包的路径" 这种方式引用包,仅执行包的初始化函数,即使包没有 init 初始化函数,也不会引发编译器报错。

示例代码如下:

  1. package main
  2. import (
  3. _ "database/sql"
  4. "fmt"
  5. )
  6. func main() {
  7. fmt.Println("C语言中文网")
  8. }

注意:

  • 一个包可以有多个 init 函数,包加载时会执行全部的 init 函数,但并不能保证执行顺序,所以不建议在一个包中放入多个 init 函数,将需要初始化的逻辑放到一个 init 函数里面。
  • 包不能出现环形引用的情况,比如包 a 引用了包 b,包 b 引用了包 c,如果包 c 又引用了包 a,则编译不能通过。
  • 包的重复引用是允许的,比如包 a 引用了包 b 和包 c,包 b 和包 c 都引用了包 d。这种场景相当于重复引用了 d,这种情况是允许的,并且 Go 编译器保证包 d 的 init 函数只会执行一次。

包加载

通过前面一系列的学习相信大家已经大体了解了 Go 程序的启动和加载过程,在执行 main 包的 mian 函数之前, Go 引导程序会先对整个程序的包进行初始化。整个执行的流程如下图所示。

Go语言包的初始化有如下特点:

    • 包初始化程序从 main 函数引用的包开始,逐级查找包的引用,直到找到没有引用其他包的包,最终生成一个包引用的有向无环图。
    • Go 编译器会将有向无环图转换为一棵树,然后从树的叶子节点开始逐层向上对包进行初始化。
    • 单个包的初始化过程如上图所示,先初始化常量,然后是全局变量,最后执行包的 init 函数。

Go语言封装简介及实现细节

在Go语言中封装就是把抽象出来的字段和对字段的操作封装在一起,数据被保护在内部,程序的其它包只能通过被授权的方法,才能对字段进行操作。

封装的好处:

  • 隐藏实现细节;
  • 可以对数据进行验证,保证数据安全合理。


如何体现封装:

  • 对结构体中的属性进行封装;
  • 通过方法,包,实现封装。


封装的实现步骤:

    • 将结构体、字段的首字母小写;
    • 给结构体所在的包提供一个工厂模式的函数,首字母大写,类似一个构造函数;
    • 提供一个首字母大写的 Set 方法(类似其它语言的 public),用于对属性判断并赋值;
    • 提供一个首字母大写的 Get 方法(类似其它语言的 public),用于获取属性的值。

Go语言GOPATH详解(Go语言工作目录)

GOPATH 是 Go语言中使用的一个环境变量,它使用绝对路径提供项目的工作目录。

工作目录是一个工程开发的相对参考目录,好比当你要在公司编写一套服务器代码,你的工位所包含的桌面、计算机及椅子就是你的工作区。工作区的概念与工作目录的概念也是类似的。如果不使用工作目录的概念,在多人开发时,每个人有一套自己的目录结构,读取配置文件的位置不统一,输出的二进制运行文件也不统一,这样会导致开发的标准不统一,影响开发效率。

GOPATH 适合处理大量 Go语言源码、多个包组合而成的复杂工程。

使用命令行查看GOPATH信息

在《安装Go语言开发包》一节中我们已经介绍过 Go语言的安装方法。在安装过 Go 开发包的操作系统中,可以使用命令行查看 Go 开发包的环境变量配置信息,这些配置信息里可以查看到当前的 GOPATH 路径设置情况。在命令行中运行go env后,命令行将提示以下信息:

$ go env
GOARCH="amd64"
GOBIN=""
GOEXE=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOOS="linux"
GOPATH="/home/davy/go"
GORACE=""
GOROOT="/usr/local/go"
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
CC="gcc"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0"
CXX="g++"
CGO_ENABLED="1"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"

命令行说明如下:

  • 第 1 行,执行 go env 指令,将输出当前 Go 开发包的环境变量状态。
  • 第 2 行,GOARCH 表示目标处理器架构。
  • 第 3 行,GOBIN 表示编译器和链接器的安装位置。
  • 第 7 行,GOOS 表示目标操作系统。
  • 第 8 行,GOPATH 表示当前工作目录。
  • 第 10 行,GOROOT 表示 Go 开发包的安装目录。


从命令行输出中,可以看到 GOPATH 设定的路径为:/home/davy/go(davy 为笔者的用户名)。

使用GOPATH的工程结构

在 GOPATH 指定的工作目录下,代码总是会保存在 $GOPATH/src 目录下。在工程经过 go build、go install 或 go get 等指令后,会将产生的二进制可执行文件放在 $GOPATH/bin 目录下,生成的中间缓存文件会被保存在 $GOPATH/pkg 下。

如果需要将整个源码添加到版本管理工具(Version Control System,VCS)中时,只需要添加 $GOPATH/src 目录的源码即可。bin 和 pkg 目录的内容都可以由 src 目录生成。

Go语言常用内置包简介

标准的Go语言代码库中包含了大量的包,并且在安装 Go 的时候多数会自动安装到系统中。我们可以在 $GOROOT/src/pkg 目录中查看这些包。下面简单介绍一些我们开发中常用的包。

1) fmt

fmt 包实现了格式化的标准输入输出,这与C语言中的 printf 和 scanf 类似。其中的 fmt.Printf() 和 fmt.Println() 是开发者使用最为频繁的函数。

格式化短语派生于C语言,一些短语(%- 序列)是这样使用:

  • %v:默认格式的值。当打印结构时,加号(%+v)会增加字段名;
  • %#v:Go样式的值表达;
  • %T:带有类型的 Go 样式的值表达。

2) io

这个包提供了原始的 I/O 操作界面。它主要的任务是对 os 包这样的原始的 I/O 进行封装,增加一些其他相关,使其具有抽象功能用在公共的接口上。

3) bufio

bufio 包通过对 io 包的封装,提供了数据缓冲功能,能够一定程度减少大块数据读写带来的开销。

在 bufio 各个组件内部都维护了一个缓冲区,数据读写操作都直接通过缓存区进行。当发起一次读写操作时,会首先尝试从缓冲区获取数据,只有当缓冲区没有数据时,才会从数据源获取数据更新缓冲。

4) sort

sort 包提供了用于对切片和用户定义的集合进行排序的功能。

5) strconv

strconv 包提供了将字符串转换成基本数据类型,或者从基本数据类型转换为字符串的功能。

6) os

os 包提供了不依赖平台的操作系统函数接口,设计像 Unix 风格,但错误处理是 go 风格,当 os 包使用时,如果失败后返回错误类型而不是错误数量。

7) sync

sync 包实现多线程中锁机制以及其他同步互斥机制。

8) flag

flag 包提供命令行参数的规则定义和传入参数解析的功能。绝大部分的命令行程序都需要用到这个包。

9) encoding/json

JSON 目前广泛用做网络程序中的通信格式。encoding/json 包提供了对 JSON 的基本支持,比如从一个对象序列化为 JSON 字符串,或者从 JSON 字符串反序列化出一个具体的对象等。

10) html/template

主要实现了 web 开发中生成 html 的 template 的一些函数。

11) net/http

net/http 包提供 HTTP 相关服务,主要包括 http 请求、响应和 URL 的解析,以及基本的 http 客户端和扩展的 http 服务。

通过 net/http 包,只需要数行代码,即可实现一个爬虫或者一个 Web 服务器,这在传统语言中是无法想象的。

12) reflect

reflect 包实现了运行时反射,允许程序通过抽象类型操作对象。通常用于处理静态类型 interface{} 的值,并且通过 Typeof 解析出其动态类型信息,通常会返回一个有接口类型 Type 的对象。

13) os/exec

os/exec 包提供了执行自定义 linux 命令的相关实现。

14) strings

strings 包主要是处理字符串的一些函数集合,包括合并、查找、分割、比较、后缀检查、索引、大小写处理等等。

strings 包与 bytes 包的函数接口功能基本一致。

15) bytes

bytes 包提供了对字节切片进行读写操作的一系列函数。字节切片处理的函数比较多,分为基本处理函数、比较函数、后缀检查函数、索引函数、分割函数、大小写处理函数和子切片处理函数等。

16) log

log 包主要用于在程序中输出日志。

log 包中提供了三类日志输出接口,Print、Fatal 和 Panic。

    • Print 是普通输出;
    • Fatal 是在执行完 Print 后,执行 os.Exit(1);
    • Panic 是在执行完 Print 后调用 panic() 方法。

Go语言sync包与锁:限制线程对变量的访问

Go语言中 sync 包里提供了互斥锁 Mutex 和读写锁 RWMutex 用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况。

为什么需要锁

锁是 sync 包中的核心,它主要有两个方法,分别是加锁(Lock)和解锁(Unlock)。

在并发的情况下,多个线程或协程同时其修改一个变量,使用锁能保证在某一时间内,只有一个协程或线程修改这一变量。

不使用锁时,在并发的情况下可能无法得到想要的结果,如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. var a = 0
  8. for i := 0; i < 1000; i++ {
  9. go func(idx int) {
  10. a += 1
  11. fmt.Println(a)
  12. }(i)
  13. }
  14. time.Sleep(time.Second)
  15. }

从理论上来说,上面的程序会将 a 的值依次递增输出,然而实际结果却是下面这样子的。

537
995
996
997
538
999
1000

通过运行结果可以看出 a 的值并不是按顺序递增输出的,这是为什么呢?

协程的执行顺序大致如下所示:

  • 从寄存器读取 a 的值;
  • 然后做加法运算;
  • 最后写到寄存器。


按照上面的顺序,假如有一个协程取得 a 的值为 3,然后执行加法运算,此时又有一个协程对 a 进行取值,得到的值同样是 3,最终两个协程的返回结果是相同的。

而锁的概念就是,当一个协程正在处理 a 时将 a 锁定,其它协程需要等待该协程处理完成并将 a 解锁后才能再进行操作,也就是说同时处理 a 的协程只能有一个,从而避免上面示例中的情况出现。 

互斥锁 Mutex

上面的示例中出现的问题怎么解决呢?加一个互斥锁 Mutex 就可以了。那什么是互斥锁呢 ?互斥锁中其有两个方法可以调用,如下所示:

func (m *Mutex) Lock()
func (m *Mutex) Unlock()

将上面的代码略作修改,如下所示:

  1. package main
  2. import (
  3.     "fmt"
  4.     "sync"
  5.     "time"
  6. )
  7. func main() {
  8.     var a = 0
  9.     var lock sync.Mutex
  10.     for i := 0; i < 1000; i++ {
  11.         go func(idx int) {
  12.             lock.Lock()
  13.             defer lock.Unlock()
  14.             a += 1
  15.             fmt.Printf("goroutine %d, a=%d\n", idx, a)
  16.         }(i)
  17.     }
  18.     // 等待 1s 结束主程序
  19.     // 确保所有协程执行完
  20.     time.Sleep(time.Second)
  21. }

运行结果如下:

goroutine 995, a=996
goroutine 996, a=997
goroutine 997, a=998
goroutine 998, a=999
goroutine 999, a=1000

需要注意的是一个互斥锁只能同时被一个 goroutine 锁定,其它 goroutine 将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定),示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "sync"
  5. "time"
  6. )
  7. func main() {
  8. ch := make(chan struct{}, 2)
  9. var l sync.Mutex
  10. go func() {
  11. l.Lock()
  12. defer l.Unlock()
  13. fmt.Println("goroutine1: 我会锁定大概 2s")
  14. time.Sleep(time.Second * 2)
  15. fmt.Println("goroutine1: 我解锁了,你们去抢吧")
  16. ch <- struct{}{}
  17. }()
  18. go func() {
  19. fmt.Println("goroutine2: 等待解锁")
  20. l.Lock()
  21. defer l.Unlock()
  22. fmt.Println("goroutine2: 欧耶,我也解锁了")
  23. ch <- struct{}{}
  24. }()
  25. // 等待 goroutine 执行结束
  26. for i := 0; i < 2; i++ {
  27. <-ch
  28. }
  29. }

上面的代码运行结果如下:

goroutine1: 我会锁定大概 2s
goroutine2: 等待解锁
goroutine1: 我解锁了,你们去抢吧
goroutine2: 欧耶,我也解锁了

读写锁

读写锁有如下四个方法:

  • 写操作的锁定和解锁分别是func (*RWMutex) Lockfunc (*RWMutex) Unlock
  • 读操作的锁定和解锁分别是func (*RWMutex) Rlockfunc (*RWMutex) RUnlock


读写锁的区别在于:

  • 当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;
  • 当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;
  • 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。


所以说这里的读锁定(RLock)目的其实是告诉写锁定,有很多协程或者进程正在读取数据,写操作需要等它们读(读解锁)完才能进行写(写锁定)。

我们可以将其总结为如下三条:

  • 同时只能有一个 goroutine 能够获得写锁定;
  • 同时可以有任意多个 gorouinte 获得读锁定;
  • 同时只能存在写锁定或读锁定(读和写互斥)。


示例代码如下所示:

  1. package main
  2. import (
  3. "fmt"
  4. "math/rand"
  5. "sync"
  6. )
  7. var count int
  8. var rw sync.RWMutex
  9. func main() {
  10. ch := make(chan struct{}, 10)
  11. for i := 0; i < 5; i++ {
  12. go read(i, ch)
  13. }
  14. for i := 0; i < 5; i++ {
  15. go write(i, ch)
  16. }
  17. for i := 0; i < 10; i++ {
  18. <-ch
  19. }
  20. }
  21. func read(n int, ch chan struct{}) {
  22. rw.RLock()
  23. fmt.Printf("goroutine %d 进入读操作...\n", n)
  24. v := count
  25. fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
  26. rw.RUnlock()
  27. ch <- struct{}{}
  28. }
  29. func write(n int, ch chan struct{}) {
  30. rw.Lock()
  31. fmt.Printf("goroutine %d 进入写操作...\n", n)
  32. v := rand.Intn(1000)
  33. count = v
  34. fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
  35. rw.Unlock()
  36. ch <- struct{}{}
  37. }

其执行结果如下:

goroutine 0 进入读操作...
goroutine 0 读取结束,值为:0
goroutine 3 进入读操作...
goroutine 1 进入读操作...
goroutine 3 读取结束,值为:0
goroutine 1 读取结束,值为:0
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:81
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:59
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:81

下面再来看两个示例。

【示例 1】多个读操作同时读取一个变量时,虽然加了锁,但是读操作是不受影响的。(读和写是互斥的,读和读不互斥)

  1. package main
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. var m *sync.RWMutex
  7. func main() {
  8. m = new(sync.RWMutex)
  9. // 多个同时读
  10. go read(1)
  11. go read(2)
  12. time.Sleep(2*time.Second)
  13. }
  14. func read(i int) {
  15. println(i,"read start")
  16. m.RLock()
  17. println(i,"reading")
  18. time.Sleep(1*time.Second)
  19. m.RUnlock()
  20. println(i,"read over")
  21. }

运行结果如下:

1 read start
1 reading
2 read start
2 reading
1 read over
2 read over

【示例 2】由于读写互斥,所以写操作开始的时候,读操作必须要等写操作进行完才能继续,不然读操作只能继续等待。

  1. package main
  2. import (
  3. "sync"
  4. "time"
  5. )
  6. var m *sync.RWMutex
  7. func main() {
  8. m = new(sync.RWMutex)
  9. // 写的时候啥也不能干
  10. go write(1)
  11. go read(2)
  12. go write(3)
  13. time.Sleep(2*time.Second)
  14. }
  15. func read(i int) {
  16. println(i,"read start")
  17. m.RLock()
  18. println(i,"reading")
  19. time.Sleep(1*time.Second)
  20. m.RUnlock()
  21. println(i,"read over")
  22. }
  23. func write(i int) {
  24. println(i,"write start")
  25. m.Lock()
  26. println(i,"writing")
  27. time.Sleep(1*time.Second)
  28. m.Unlock()
  29. println(i,"write over")
  30. }

运行结果如下:

1 write start
3 write start
1 writing
2 read start
1 write over
2 reading

Go语言big包:对整数的高精度计算

实际开发中,对于超出 int64 或者 uint64 类型的大数进行计算时,如果对精度没有要求,使用 float32 或者 float64 就可以胜任,但如果对精度有严格要求的时候,我们就不能使用浮点数了,因为浮点数在内存中只能被近似的表示。

Go语言中 math/big 包实现了大数字的多精度计算,支持 Int(有符号整数)、Rat(有理数)和 Float(浮点数)等数字类型。

这些类型可以实现任意位数的数字,只要内存足够大,但缺点是需要更大的内存和处理开销,这使得它们使用起来要比内置的数字类型慢很多。

Go语言正则表达式:regexp包

正则表达式是一种进行模式匹配和文本操纵的复杂而又强大的工具。虽然正则表达式比纯粹的文本匹配效率低,但是它却更灵活,按照它的语法规则,根据需求构造出的正则表达式能够从原始文本中筛选出几乎任何你想要得到的字符组合。

Go语言通过 regexp 包为正则表达式提供了官方支持,其采用 RE2 语法,除了\c\C外,Go语言和 Perl、Python 等语言的正则基本一致。

正则表达式语法规则

正则表达式是由普通字符(例如字符 a 到 z)以及特殊字符(称为"元字符")构成的文字序列,可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。

下面的表格中列举了构成正则表达式的一些语法规则及其含义。

1) 字符

语法说明表达式示例匹配结果
一般字符 匹配自身 abc abc
. 匹配任意除换行符"\n"外的字符, 在 DOTALL 模式中也能匹配换行符 a.c abc
\ 转义字符,使后一个字符改变原来的意思;
如果字符串中有字符 * 需要匹配,可以使用 \* 或者字符集[*]。
a\.c
a\\c
a.c
a\c
[...] 字符集(字符类),对应的位置可以是字符集中任意字符。
字符集中的字符可以逐个列出,也可以给出范围,如 [abc] 或 [a-c],
第一个字符如果是 ^ 则表示取反,如 [^abc] 表示除了abc之外的其他字符。
a[bcd]e abe 或 ace 或 ade
\d 数字:[0-9] a\dc a1c
\D 非数字:[^\d] a\Dc abc
\s 空白字符:[<空格>\t\r\n\f\v] a\sc a c
\S 非空白字符:[^\s] a\Sc abc
\w 单词字符:[A-Za-z0-9] a\wc abc
\W 非单词字符:[^\w] a\Wc a c

2) 数量词(用在字符或 (...) 之后)

语法说明表达式示例匹配结果
* 匹配前一个字符 0 或无限次 abc* ab 或 abccc
+ 匹配前一个字符 1 次或无限次 abc+ abc 或 abccc
? 匹配前一个字符 0 次或 1 次 abc? ab 或 abc
{m} 匹配前一个字符 m 次 ab{2}c abbc
{m,n} 匹配前一个字符 m 至 n 次,m 和 n 可以省略,若省略 m,则匹配 0 至 n 次;
若省略 n,则匹配 m 至无限次
ab{1,2}c abc 或 abbc

3) 边界匹配

语法说明表达式示例匹配结果
^ 匹配字符串开头,在多行模式中匹配每一行的开头 ^abc abc
$ 匹配字符串末尾,在多行模式中匹配每一行的末尾 abc$ abc
\A 仅匹配字符串开头 \Aabc abc
\Z 仅匹配字符串末尾 abc\Z abc
\b 匹配 \w 和 \W 之间 a\b!bc a!bc
\B [^\b] a\Bbc abc

4) 逻辑、分组

语法说明表达式示例匹配结果
| | 代表左右表达式任意匹配一个,优先匹配左边的表达式 abc|def abc 或 def
(...) 括起来的表达式将作为分组,分组将作为一个整体,可以后接数量词 (abc){2} abcabc
(?P<name>...) 分组,功能与 (...) 相同,但会指定一个额外的别名 (?P<id>abc){2} abcabc
\<number> 引用编号为 <number> 的分组匹配到的字符串 (\d)abc\1 1abe1 或 5abc5
(?P=name) 引用别名为 <name> 的分组匹配到的字符串 (?P<id>\d)abc(?P=id) 1abe1 或 5abc5

5) 特殊构造(不作为分组)

语法说明表达式示例匹配结果
(?:...) (…) 的不分组版本,用于使用 "|" 或后接数量词 (?:abc){2} abcabc
(?iLmsux) iLmsux 中的每个字符代表一种匹配模式,只能用在正则表达式的开头,可选多个 (?i)abc AbC
(?#...) # 后的内容将作为注释被忽略。 abc(?#comment)123  abc123
(?=...) 之后的字符串内容需要匹配表达式才能成功匹配 a(?=\d) 后面是数字的 a
(?!...) 之后的字符串内容需要不匹配表达式才能成功匹配 a(?!\d) 后面不是数字的 a
(?<=...) 之前的字符串内容需要匹配表达式才能成功匹配 (?<=\d)a 前面是数字的a
(?<!...) 之前的字符串内容需要不匹配表达式才能成功匹配 (?<!\d)a 前面不是数字的a

Regexp 包的使用

下面通过几个示例来演示一下 regexp 包的使用。

【示例 1】匹配指定类型的字符串。

  1. package main
  2. import (
  3. "fmt"
  4. "regexp"
  5. )
  6. func main() {
  7. buf := "abc azc a7c aac 888 a9c tac"
  8. //解析正则表达式,如果成功返回解释器
  9. reg1 := regexp.MustCompile(`a.c`)
  10. if reg1 == nil {
  11. fmt.Println("regexp err")
  12. return
  13. }
  14. //根据规则提取关键信息
  15. result1 := reg1.FindAllStringSubmatch(buf, -1)
  16. fmt.Println("result1 = ", result1)
  17. }

运行结果如下:

result1 =  [[abc] [azc] [a7c] [aac] [a9c]]  

Go语言time包:时间和日期

上面代码中:

  • wall:表示距离公元 1 年 1 月 1 日 00:00:00UTC 的秒数;
  • ext:表示纳秒;
  • loc:代表时区,主要处理偏移量,不同的时区,对应的时间不一样。

如何正确表示时间呢?

公认最准确的计算应该是使用“原子震荡周期”所计算的物理时钟了(Atomic Clock, 也被称为原子钟),这也被定义为标准时间(International Atomic Time)。

而我们常常看见的 UTC(Universal Time Coordinated,世界协调时间)就是利用这种 Atomic Clock 为基准所定义出来的正确时间。UTC 标准时间是以 GMT(Greenwich Mean Time,格林尼治时间)这个时区为主,所以本地时间与 UTC 时间的时差就是本地时间与 GMT 时间的时差。

UTC + 时区差 = 本地时间

国内一般使用的是北京时间,与 UTC 的时间关系如下:

UTC + 8 个小时 = 北京时间

在Go语言的 time 包里面有两个时区变量,如下:

  • time.UTC:UTC 时间
  • time.Local:本地时间


同时,Go语言还提供了 LoadLocation 方法和 FixedZone 方法来获取时区变量,如下:

FixedZone(name string, offset int) *Location

其中,name 为时区名称,offset 是与 UTC 之前的时差。

LoadLocation(name string) (*Location, error)

其中,name 为时区的名字。

时间的获取

1) 获取当前时间

我们可以通过 time.Now() 函数来获取当前的时间对象,然后通过事件对象来获取当前的时间信息。示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. now := time.Now() //获取当前时间
  8. fmt.Printf("current time:%v\n", now)
  9. year := now.Year() //年
  10. month := now.Month() //月
  11. day := now.Day() //日
  12. hour := now.Hour() //小时
  13. minute := now.Minute() //分钟
  14. second := now.Second() //秒
  15. fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
  16. }

运行结果如下:

current time:2019-12-12 12:33:19.4712277 +0800 CST m=+0.006980401
2019-12-12 12:33:19

2) 获取时间戳

时间戳是自 1970 年 1 月 1 日(08:00:00GMT)至当前时间的总毫秒数,它也被称为 Unix 时间戳(UnixTimestamp)。

基于时间对象获取时间戳的示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. now := time.Now() //获取当前时间
  8. timestamp1 := now.Unix() //时间戳
  9. timestamp2 := now.UnixNano() //纳秒时间戳
  10. fmt.Printf("现在的时间戳:%v\n", timestamp1)
  11. fmt.Printf("现在的纳秒时间戳:%v\n", timestamp2)
  12. }

运行结果如下:

现在的时间戳:1576127858
现在的纳秒时间戳:1576127858829900100

使用 time.Unix() 函数可以将时间戳转为时间格式,示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. now := time.Now() //获取当前时间
  8. timestamp := now.Unix() //时间戳
  9. timeObj := time.Unix(timestamp, 0) //将时间戳转为时间格式
  10. fmt.Println(timeObj)
  11. year := timeObj.Year() //年
  12. month := timeObj.Month() //月
  13. day := timeObj.Day() //日
  14. hour := timeObj.Hour() //小时
  15. minute := timeObj.Minute() //分钟
  16. second := timeObj.Second() //秒
  17. fmt.Printf("%d-%02d-%02d %02d:%02d:%02d\n", year, month, day, hour, minute, second)
  18. }

运行结果如下:

2019-12-12 13:24:09 +0800 CST
2019-12-12 13:24:09

3) 获取当前是星期几

time 包中的 Weekday 函数能够返回某个时间点所对应是一周中的周几,示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. //时间戳
  8. t := time.Now()
  9. fmt.Println(t.Weekday().String())
  10. }

运行结果如下:

Thursday

时间操作函数

1) Add

我们在日常的开发过程中可能会遇到要求某个时间 + 时间间隔之类的需求,Go语言中的 Add 方法如下:

func (t Time) Add(d Duration) Time

Add 函数可以返回时间点 t + 时间间隔 d 的值。

【示例】求一个小时之后的时间:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. now := time.Now()
  8. later := now.Add(time.Hour) // 当前时间加1小时后的时间
  9. fmt.Println(later)
  10. }

运行结果如下:

2019-12-12 16:00:29.9866943 +0800 CST m=+3600.007978201

2) Sub

求两个时间之间的差值:

func (t Time) Sub(u Time) Duration

返回一个时间段 t - u 的值。如果结果超出了 Duration 可以表示的最大值或最小值,将返回最大值或最小值,要获取时间点 t - d(d 为 Duration),可以使用 t.Add(-d)。

3) Equal

判断两个时间是否相同:

func (t Time) Equal(u Time) bool

Equal 函数会考虑时区的影响,因此不同时区标准的时间也可以正确比较,Equal 方法和用 t==u 不同,Equal 方法还会比较地点和时区信息。

4) Before

判断一个时间点是否在另一个时间点之前:

func (t Time) Before(u Time) bool

如果 t 代表的时间点在 u 之前,则返回真,否则返回假。

5) After

判断一个时间点是否在另一个时间点之后:

func (t Time) After(u Time) bool

如果 t 代表的时间点在 u 之后,则返回真,否则返回假。

定时器

使用 time.Tick(时间间隔) 可以设置定时器,定时器的本质上是一个通道(channel),示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
  8. for i := range ticker {
  9. fmt.Println(i) //每秒都会执行的任务
  10. }
  11. }

运行结果如下:

2019-12-12 15:14:26.4158067 +0800 CST m=+16.007460701
2019-12-12 15:14:27.4159467 +0800 CST m=+17.007600701
2019-12-12 15:14:28.4144689 +0800 CST m=+18.006122901
2019-12-12 15:14:29.4159581 +0800 CST m=+19.007612101
2019-12-12 15:14:30.4144337 +0800 CST m=+20.006087701
...

时间格式化

时间类型有一个自带的 Format 方法进行格式化,需要注意的是Go语言中格式化时间模板不是常见的Y-m-d H:M:S 而是使用Go语言的诞生时间 2006 年 1 月 2 号 15 点 04 分 05 秒。

提示:如果想将时间格式化为 12 小时格式,需指定 PM。

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. now := time.Now()
  8. // 格式化的模板为Go的出生时间2006年1月2号15点04分 Mon Jan
  9. // 24小时制
  10. fmt.Println(now.Format("2006-01-02 15:04:05.000 Mon Jan"))
  11. // 12小时制
  12. fmt.Println(now.Format("2006-01-02 03:04:05.000 PM Mon Jan"))
  13. fmt.Println(now.Format("2006/01/02 15:04"))
  14. fmt.Println(now.Format("15:04 2006/01/02"))
  15. fmt.Println(now.Format("2006/01/02"))
  16. }

运行结果如下:

2019-12-12 15:20:52.037 Thu Dec
2019-12-12 03:20:52.037 PM Thu Dec
2019/12/12 15:20
15:20 2019/12/12
2019/12/12

解析字符串格式的时间

Parse 函数可以解析一个格式化的时间字符串并返回它代表的时间。

func Parse(layout, value string) (Time, error)

与 Parse 函数类似的还有 ParseInLocation 函数。

func ParseInLocation(layout, value string, loc *Location) (Time, error)

ParseInLocation 与 Parse 函数类似,但有两个重要的不同之处:

  • 第一,当缺少时区信息时,Parse 将时间解释为 UTC 时间,而 ParseInLocation 将返回值的 Location 设置为 loc;
  • 第二,当时间字符串提供了时区偏移量信息时,Parse 会尝试去匹配本地时区,而 ParseInLocation 会去匹配 loc。


示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "time"
  5. )
  6. func main() {
  7. var layout string = "2006-01-02 15:04:05"
  8. var timeStr string = "2019-12-12 15:22:12"
  9. timeObj1, _ := time.Parse(layout, timeStr)
  10. fmt.Println(timeObj1)
  11. timeObj2, _ := time.ParseInLocation(layout, timeStr, time.Local)
  12. fmt.Println(timeObj2)
  13. }

运行结果如下:

2019-12-12 15:22:12 +0000 UTC
2019-12-12 15:22:12 +0800 CST

Go语言os包用法简述

Go语言的 os 包中提供了操作系统函数的接口,是一个比较重要的包。顾名思义,os 包的作用主要是在服务器上进行系统的基本操作,如文件操作、目录操作、执行命令、信号与中断、进程、系统状态等等。

os 包中的常用函数

1) Hostname

函数定义:

func Hostname() (name string, err error)

Hostname 函数会返回内核提供的主机名。

2) Environ

函数定义:

func Environ() []string

Environ 函数会返回所有的环境变量,返回值格式为“key=value”的字符串的切片拷贝。

3) Getenv

函数定义:

func Getenv(key string) string

Getenv 函数会检索并返回名为 key 的环境变量的值。如果不存在该环境变量则会返回空字符串。

4) Setenv

函数定义:

func Setenv(key, value string) error

Setenv 函数可以设置名为 key 的环境变量,如果出错会返回该错误。

5) Exit

函数定义:

func Exit(code int)

Exit 函数可以让当前程序以给出的状态码 code 退出。一般来说,状态码 0 表示成功,非 0 表示出错。程序会立刻终止,并且 defer 的函数不会被执行。

6) Getuid

函数定义:

func Getuid() int

Getuid 函数可以返回调用者的用户 ID。

7) Getgid

函数定义:

func Getgid() int

Getgid 函数可以返回调用者的组 ID。

8) Getpid

函数定义:

func Getpid() int

Getpid 函数可以返回调用者所在进程的进程 ID。

9) Getwd

函数定义:

func Getwd() (dir string, err error)

Getwd 函数可以返回一个对应当前工作目录的根路径。如果当前目录可以经过多条路径抵达(因为硬链接),Getwd 会返回其中一个。

10) Mkdir

函数定义:

func Mkdir(name string, perm FileMode) error

Mkdir 函数可以使用指定的权限和名称创建一个目录。如果出错,会返回 *PathError 底层类型的错误。

11) MkdirAll

函数定义:

func MkdirAll(path string, perm FileMode) error

MkdirAll 函数可以使用指定的权限和名称创建一个目录,包括任何必要的上级目录,并返回 nil,否则返回错误。权限位 perm 会应用在每一个被该函数创建的目录上。如果 path 指定了一个已经存在的目录,MkdirAll 不做任何操作并返回 nil。

12) Remove

函数定义:

func Remove(name string) error

Remove 函数会删除 name 指定的文件或目录。如果出错,会返回 *PathError 底层类型的错误。

RemoveAll 函数跟 Remove 用法一样,区别是会递归的删除所有子目录和文件。

在 os 包下,有 exec,signal,user 三个子包,下面来分别介绍一下。

os/exec 执行外部命令

exec 包可以执行外部命令,它包装了 os.StartProcess 函数以便更容易的修正输入和输出,使用管道连接 I/O,以及作其它的一些调整。

func LookPath(file string) (string, error)

在环境变量 PATH 指定的目录中搜索可执行文件,如果 file 中有斜杠,则只在当前目录搜索。返回完整路径或者相对于当前目录的一个相对路径。

示例代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "os/exec"
  5. )
  6. func main() {
  7. f, err := exec.LookPath("main")
  8. if err != nil {
  9. fmt.Println(err)
  10. }
  11. fmt.Println(f)
  12. }

运行结果如下:

main.exe

os/user 获取当前用户信息

可以通过 os/user 包中的 Current() 函数来获取当前用户信息,该函数会返回一个 User 结构体,结构体中的 Username、Uid、HomeDir、Gid 分别表示当前用户的名称、用户 id、用户主目录和用户所属组 id,函数原型如下:

func Current() (*User, error)

示例代码如下:

  1. package main
  2. import (
  3. "log"
  4. "os/user"
  5. )
  6. func main() {
  7. u, _ := user.Current()
  8. log.Println("用户名:", u.Username)
  9. log.Println("用户id", u.Uid)
  10. log.Println("用户主目录:", u.HomeDir)
  11. log.Println("主组id:", u.Gid)
  12. // 用户所在的所有的组的id
  13. s, _ := u.GroupIds()
  14. log.Println("用户所在的所有组:", s)
  15. }

运行结果如下:

2019/12/13 15:12:14 用户名: LENOVO-PC\Administrator
2019/12/13 15:12:14 用户id S-1-5-21-711400000-2334436127-1750000211-000
2019/12/13 15:12:14 用户主目录: C:\Users\Administrator
2019/12/13 15:12:14 主组id: S-1-5-22-766000000-2300000100-1050000262-000
2019/12/13 15:12:14 用户所在的所有组: [S-1-5-32-544 S-1-5-22-000 S-1-5-21-777400999-2344436111-1750000262-003]

os/signal 信号处理

一个运行良好的程序在退出(正常退出或者强制退出,如 Ctrl+C,kill 等)时是可以执行一段清理代码的,将收尾工作做完后再真正退出。一般采用系统 Signal 来通知系统退出,如 kill pid,在程序中针对一些系统信号设置了处理函数,当收到信号后,会执行相关清理程序或通知各个子进程做自清理。

Go语言中对信号的处理主要使用 os/signal 包中的两个方法,一个是 Notify 方法用来监听收到的信号,一个是 stop 方法用来取消监听。

func Notify(c chan<- os.Signal, sig ...os.Signal)

其中,第一个参数表示接收信号的 channel,第二个及后面的参数表示设置要监听的信号,如果不设置表示监听所有的信号。

【示例 1】使用 Notify 方法来监听收到的信号:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "os/signal"
  6. )
  7. func main() {
  8. c := make(chan os.Signal, 0)
  9. signal.Notify(c)
  10. // Block until a signal is received.
  11. s := <-c
  12. fmt.Println("Got signal:", s)
  13. }

运行该程序,然后在 CMD 窗口中通过 Ctrl+C 来结束该程序,便会得到输出结果:

Got signal: interrupt

【示例 2】使用 stop 方法来取消监听:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "os/signal"
  6. )
  7. func main() {
  8. c := make(chan os.Signal, 0)
  9. signal.Notify(c)
  10. signal.Stop(c) //不允许继续往c中存入内容
  11. s := <-c //c无内容,此处阻塞,所以不会执行下面的语句,也就没有输出
  12. fmt.Println("Got signal:", s)
  13. }

因为使用 Stop 方法取消了 Notify 方法的监听,所以运行程序没有输出结果。

Go语言go mod包依赖管理工具使用详解

最早的时候,Go语言所依赖的所有的第三方库都放在 GOPATH 这个目录下面,这就导致了同一个库只能保存一个版本的代码。如果不同的项目依赖同一个第三方的库的不同版本,应该怎么解决?

go module 是Go语言从 1.11 版本之后官方推出的版本管理工具,并且从 Go1.13 版本开始,go module 成为了Go语言默认的依赖管理工具。

Modules 官方定义为:

Modules 是相关 Go 包的集合,是源代码交换和版本控制的单元。Go语言命令直接支持使用 Modules,包括记录和解析对其他模块的依赖性,Modules 替换旧的基于 GOPATH 的方法,来指定使用哪些源文件。

如何使用 Modules?

1) 首先需要把 golang 升级到 1.11 版本以上(现在 1.13 已经发布了,建议使用 1.13)。

2) 设置 GO111MODULE。

GO111MODULE

在Go语言 1.12 版本之前,要启用 go module 工具首先要设置环境变量 GO111MODULE,不过在Go语言 1.13 及以后的版本则不再需要设置环境变量。通过 GO111MODULE 可以开启或关闭 go module 工具。

  • GO111MODULE=off 禁用 go module,编译时会从 GOPATH 和 vendor 文件夹中查找包;
  • GO111MODULE=on 启用 go module,编译时会忽略 GOPATH 和 vendor 文件夹,只根据 go.mod下载依赖;
  • GO111MODULE=auto(默认值),当项目在 GOPATH/src 目录之外,并且项目根目录有 go.mod 文件时,开启 go module。


Windows 下开启 GO111MODULE 的命令为:

set GO111MODULE=on 或者 set GO111MODULE=auto

MacOS 或者 Linux 下开启 GO111MODULE 的命令为:

export GO111MODULE=on 或者 export GO111MODULE=auto

在开启 GO111MODULE 之后就可以使用 go module 工具了,也就是说在以后的开发中就没有必要在 GOPATH 中创建项目了,并且还能够很好的管理项目依赖的第三方包信息。

常用的go mod命令如下表所示:

命令作用
go mod download 下载依赖包到本地(默认为 GOPATH/pkg/mod 目录)
go mod edit 编辑 go.mod 文件
go mod graph 打印模块依赖图
go mod init 初始化当前文件夹,创建 go.mod 文件
go mod tidy 增加缺少的包,删除无用的包
go mod vendor 将依赖复制到 vendor 目录下
go mod verify 校验依赖
go mod why 解释为什么需要依赖

GOPROXY

proxy 顾名思义就是代理服务器的意思。大家都知道,国内的网络有防火墙的存在,这导致有些Go语言的第三方包我们无法直接通过go get命令获取。GOPROXY 是Go语言官方提供的一种通过中间代理商来为用户提供包下载服务的方式。要使用 GOPROXY 只需要设置环境变量 GOPROXY 即可。

目前公开的代理服务器的地址有:

  • goproxy.io;
  • goproxy.cn:(推荐)由国内的七牛云提供。


Windows 下设置 GOPROXY 的命令为:

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

MacOS 或 Linux 下设置 GOPROXY 的命令为:

export GOPROXY=https://goproxy.cn

Go语言在 1.13 版本之后 GOPROXY 默认值为 https://proxy.golang.org,在国内可能会存在下载慢或者无法访问的情况,所以十分建议大家将 GOPROXY 设置为国内的 goproxy.cn。

使用go get命令下载指定版本的依赖包

执行go get 命令,在下载依赖包的同时还可以指定依赖包的版本。

  • 运行go get -u命令会将项目中的包升级到最新的次要版本或者修订版本;
  • 运行go get -u=patch命令会将项目中的包升级到最新的修订版本;
  • 运行go get [包名]@[版本号]命令会下载对应包的指定版本或者将对应包升级到指定的版本。

提示:go get [包名]@[版本号]命令中版本号可以是 x.y.z 的形式,例如 go get foo@v1.2.3,也可以是 git 上的分支或 tag,例如 go get foo@master,还可以是 git 提交时的哈希值,例如 go get foo@e3702bed2。

如何在项目中使用

【示例 1】创建一个新项目:

1) 在 GOPATH 目录之外新建一个目录,并使用go mod init初始化生成 go.mod 文件。

go mod init hello
go: creating new go.mod: module hello

go.mod 文件一旦创建后,它的内容将会被 go toolchain 全面掌控,go toolchain 会在各类命令执行时,比如go getgo buildgo mod等修改和维护 go.mod 文件。

go.mod 提供了 module、require、replace 和 exclude 四个命令:

  • module 语句指定包的名字(路径);
  • require 语句指定的依赖项模块;
  • replace 语句可以替换依赖项模块;
  • exclude 语句可以忽略依赖项模块。


初始化生成的 go.mod 文件如下所示:

module hello

go 1.13

2) 添加依赖。

新建一个 main.go 文件,写入以下代码:

  1. package main
  2. import (
  3. "net/http"
  4. "github.com/labstack/echo"
  5. )
  6. func main() {
  7. e := echo.New()
  8. e.GET("/", func(c echo.Context) error {
  9. return c.String(http.StatusOK, "Hello, World!")
  10. })
  11. e.Logger.Fatal(e.Start(":1323"))
  12. }

执行go run main.go运行代码会发现 go mod 会自动查找依赖自动下载:

 

posted @ 2022-04-07 11:02  hanease  阅读(72)  评论(0编辑  收藏  举报