从零到一开始学golang,不同于其他语言的心得记录
中文文档
- https://www.topgoer.com/
学习路径
- 官方教程 https://go.dev/learn/
- 《The Go Programming Language》 https://www.gopl.io/
- 《Concurrency in Go》 https://github.com/b055/books-1/blob/master/Concurrency%20in%20Go.pdf
- 重点学习 https://go.dev/doc/effective_go
- 代码风格 https://github.com/uber-go/guide
- 《Go in Action》 https://github.com/diptomondal007/GoLangBooks/blob/master/go-in-action.pdf
- Go 从入门到实践 http://gk.link/a/12q1w
- 技术社区 https://studygolang.com/#google_vignette
const 常量
- 常量可以用表达式(其他语言也可以)
const d = 3e20/n
- 数字常量没有类型,除非通过显式转换等方式指定类型。
// 这里的n 打印出来是int const n = 500000000 const d = 3e20 / n fmt.Println(int64(d))
- 通过在需要类型的上下文中使用数字,例如变量赋值或函数调用,可以为其指定类型
const n = 500000000 // 这里的Sin 函数需要的 float64, 用常量n未作显示转换可以直接使用 s := math.Sin(n) fmt.Println(math.Sin(n))
for
- for range 一个数字,可以直接用来指定要循环的次数,比i=0,i++优雅点(demo 上有,新版已经不支持
1.21.2测试
)// 新版已经不支持了 for i := range 3 { fmt.Println("range", i) }
if else
- 没有三元运算符
switch
- switch 不需要break
- 可以case多个值
switch time.Now().Weekday() { case time.Saturday, time.Sunday: fmt.Println("It`s the weekend") default: fmt.Println("It's a weekday") }
- switch 可以不要表达式,代替if else
t := time.Now() switch { case t.Hour() < 12: fmt.Println("It's before noon") default: fmt.Println("It's after noon") }
array
- 让编译器计算数组个数(…代替数组长度)
在赋值的情况下才可以, 单独声明不行
var f [...]int // 不支持 b = [...]int{1, 2, 3, 4, 5}
- 指定索引:,则其间的元素将被清零。
// b 再次被赋值,使用了索引3, 中间 1,2 被置为0值了 b = [...]int{100, 3: 400, 500} fmt.Println("idx:", b)
slice
-
未初始化的切片等于 nil,长度为 0 (初始值(声明&未初始化的情况下, 值为nil, len = 0))
var s []string // nil 声明未赋值 len(s) // 0
-
使用make, 初始值为零值(适合开发中先占位)
-
append 需要接受追加后的返回值,因为 append我们可能会得到一个新的切片值。
s = append(s, "e", "f")
-
copy(c,s)
将 s 考到 c
-
声明并赋值, 用来避免零值和nil 的情况
t := []string{"g", "h", "i"}
-
切片相等比较 slices.Equal(s1, s2)
if slices.Equal(t, t2) { fmt.Println("t == t2") }
map
- 键不存在则返回零值
(没有异常&索引不存在)
m := map[string]int{"k1":1} v2 = m["k2"] // 得到0 - 没有异常
- map 值删除 delete(m, “k1”)
(非 delete(m["k1"]))
- 清空所有map, clear(m)
- 相等比较,maps.Equal(m,m2)
map类型,maps包
range
- 只迭代键, 自动丢弃值
kvs := map[string]string{"a": "apple", "b": "banana"} for k := range kvs { fmt.Println("key:", k) } // key: a // key: b
- 迭代字符串(遍历 Unicode 代码点。第一个值是 的起始字节索引rune,第二个值是rune本身)
for i, c := range "go" { fmt.Println(i, c) } // 0 103 // 1 111
function
- 可变参数(num …int)
注意格式 ...int,循环时:num一个切片
func sum(nums ...int){ fmt.Println(nums," ") total := 0 for _, num := range nums { total += num } fmt.Println(total) }
- 可变参数传值方式,
多个参数
或切片(...格式)
sum(1,2) sum(1,2,3) nums := []int{1,2,3,4} sum(nums...) // 注意... 格式
闭包 closure
- 一个函数返回一个函数后,其内部的局部变量还被新函数引用
func intSeq() func()int{ i := 0 return func() int { i++ return i } } func main(){ nextInt := intSeq() // 返回一个函数,函数内部的值被外部使用, 维持闭包 fmt.Println(nextInt()) // 1 fmt.Println(nextInt()) // 2 fmt.Println(nextInt()) // 3 newInts := intSeq() // 重新初始化一个函数接收者 fmt.Println(newInts()) // 1 fmt.Println(nextInt()) // 4 上一个闭包函数内部变量值还继续维护 }
recursion 递归,函数内部调用函数自己
- 独立函数内部调用自己
func fact(n int) int { if n == 0 { return 1 } return n * fact(n-1) }
- var闭包也可以是递归的,但这要求在定义闭包之前必须用类型明确声明它。
func main() { // 通过变量 定义一个函数, 显示声明 var fib func(n int) int fib = func(n int) int { if n < 2 { return n } return fib(n-1) + fib(n-2) // 内部调用自己 } fmt.Println(fib(7)) }
- 通过变量定义的闭包函数类型,可以在任意地方直接定义,不需要先定义类型
// 该类型可以不被先定义,在声明时直接直接指定 type CF func(n int) int func main() { // 函数 func(n int) int 类型(例:`CF`)不需要先定义,可以直接在声明变量时直接显示声明 var fib func(n int) int var fib_d CF // 通过先定义类型,声明 }
pointer 指针
- 指针类型的参数,取出指针的值&设置
func zeroptr(iptr *int){ // iptr 是指针类型: zeroptr:inter: 0xc00000a0d8 fmt.Println("zeroptr:inter:",iptr) // zeroptr:inter: 0xc00000a0d8 // 指针加*, 取出指针对应的值 fmt.Println("zeroptr:inter:",*iptr) // zeroptr:inter: 1 // 前面加了*, 将指针对应的值设置成新值 *iptr = 0 }
string & rune
- 字符串等同于[]byte
- fmt.Printf(“%x”, s[i]) 打印16进制
const s = "สวัสดี" fmt.Println("Len:", len(s)) for i := 0; i < len(s); i++ { fmt.Printf("%x ", s[i]) // e0 b8 aa e0 b8 a7 e0 b8 b1 e0 b8 aa e0 b8 94 e0 b8 b5 }
- 获取字符串中有都少个utf8 字符,utf8.RuneCountInString(s)
const s = "สวัสดี" utf8.RuneCountInString(s) // 6
- 遍历utf, 通过utf8包,设置偏移量来处理(中文可以通过 range 直接遍历出来)
for i, w := 0, 0; i < len(s); i += w { // DecodeRuneInString, 返回的偏移量 runeValue, width := utf8.DecodeRuneInString(s[i:]) fmt.Printf("%#U starts at %d\n", runeValue, i) w = width }
- rune 本质是 int32, ''
单引号
字符串的值也是数字,对应ansci 码, 可以直接用来比较func examineRune(r rune) { // rune 类型 直接用来比较 if r == 't' { fmt.Println("found tee") } else if r == 'ส' { fmt.Println("found so sua") } }
struct 结构
- 定义一个结构类型,使用结构时,省略的字段将为零值
type person struct { name string age int } fmt.Println(person{name: "Fred"}) // {Fred 0}
- 匿名结构,结构类型仅用于单个值,不必为既构命名,通常用于测试单元
dog := struct { name string isGood bool }{ "Rex", true, }
- 结构的指针无法打印, 返回带&的数据结构
fmt.Println(&person{name: "Ann", age:40}) // &{Ann 40}
method 方法 (有接收者的采才叫方法,无接收者的叫函数)
- 方法的接收者可以为值类型&指针类型,但值和指针都可以调用
type rect struct { width, height int } // 指针作为接收者 func (r *rect) area() int { return r.width * r.height } // 值作为接收者 func (r rect) perim() int { return 2*r.width + 2*r.height } // 值和指针都可以调用 r := rect{width: 10,height: 5} fmt.Println("area: ", r.area()) // 值调用指针接收者 fmt.Println("perim: ", r.perim()) // 值调用值接收者
- Go自动处理方法调用时值与指针之间的转换。
使用指针接收器类型来避免方法调用时的复制,或者允许方法改变接收结构
interface 接口
- 只要实现了接口里面的方法,就相当于实现了接口(隐式继承)
// 定义了一个接口 type geometry interface { area() float64 perim() float64 } type rect struct { width, height float64 } // 结构实现了rect 实现了 area, perim 方法, 等价于实现了 geometry 接口 func (r rect) area() float64 { return r.width * r.height } func (r rect) perim() float64 { return 2*r.width + 2*r.height }
- 参数指定了 接口类型,只要实现了该接口的, 都可以作为参数
func measure(g geometry){ fmt.Println(g) fmt.Println(g.area()) fmt.Println(g.perim()) } func main(){ r := rect{3,4} // r 已经实现了 接口 geometry,该处可以直接作为参数使用了 measure(r) }
enum 枚举
- 枚举类型(枚举)是 求和类型的一种特殊情况。枚举是一种具有固定数量可能值的类型,每个值都有不同的名称。
- 定义一个 类型,实现fmt.StringerServerState接口,可以打印出 值或将其转换为字符串。
// 定义了一个类型 type ServerState int // 枚举常量组 const ( StateIdle = iota StateConnected StateError StateRetrying ) // 枚举常量可能的值 var statName = map[ServerState]string{ StateIdle: "idle", StateConnected: "connected", StateError: "error", StateRetrying: "retrying", } // 实现了 fmt.Strin 接口 func (ss ServerState) String() string { return statName[ss] }
- 自动转换demo
如果我们有一个类型的值int,我们就不能将它传递给它transition——编译器会抱怨类型不匹配。这为枚举提供了一定程度的编译时类型安全性
func main(){ ns := transition(StateIdle) fmt.Println(ns) // connected // ns 返回的时字符串,也可以用作参数 ns2 := transition(ns) fmt.Println(ns2) // idle } // ServerStatue 作为参数和放回值 实现了 string() 接口,所以返回值时 返回字符串 // 返回的字符串作为参数也是可以的 func transition(s ServerState) ServerState{ switch s { case StateIdle: return StateConnected case StateConnected, StateRetrying: return StateIdle case StateError: return StateError default: panic(fmt.Errorf("unknown state: %s", s)) } }
结构嵌入
- 直接嵌入
type base struct { num int } type container struct { base str string }
- 初始化结构创建时,明确嵌入,用作字段名称, 可以直接访问,也可以通过完整路径访问
func main() { // 明确嵌入, 指定 base:base{num:1} co := container{ base: base{ num: 1, }, str: "some name", } // 直接访问 fmt.Printf("co={num: %v, str: %v}\n", co.num, co.str) // 通过完整路径访问 fmt.Println("also num:", co.base.num) }
- 嵌入带有方法的结构可用于将接口实现赋予其他结构
type base struct { num int } // base 结构实现了desribe 方法 func (b base) describe() string { return fmt.Sprintf("base with num=%v", b.num) } // 嵌入了带有方法的结构base type container struct { base str string } func main() { co := container{ base: base{ num: 1, }, str: "some name", } type describer interface { describe() string } // container现在实现了 describer接口,因为它嵌入了base。 var d describer = co fmt.Println("describer:", d.describe()) }
泛型 generics
- 自定义泛型, 泛型格式
// 作为泛型函数的示例,MapKeys接受任何类型的映射并返回其键的切片。此函数有两个类型参数 -K和V; K具有comparable 约束,这意味着我们可以用==and !=运算符比较此类型的值。这是 Go 中映射键所必需的。 V具有any约束,这意味着它不受任何限制(any是 的别名interface{})。 func MapKeys[K comparable, V any](m map[K]V) []K { r := make([]K, 0, len(m)) for k := range m { r = append(r, k) } return r }
- 我们可以像在常规类型上一样在泛型上定义方法,但必须保留类型参数。类型是List[T],而不是List。
type List[T any] struct { head, tail *element[T] } type element[T any] struct { next *element[T] val T } func (lst *List[T]) Push(v T) { if lst.tail == nil { lst.head = &element[T]{val: v} lst.tail = lst.head } else { lst.tail.next = &element[T]{val: v} lst.tail = lst.tail.next } }
- 调用泛型函数时,可以依赖类型推断,也可以明确指定
func main(){ var m = map[int]string{1:"2",2:"4",4:"8"} // 可以依赖类型推断。请注意,我们不必在调用时指定K和的类型- 编译器会自动推断它 fmt.Println("keys:", MapKeys(m)) // 也可以明确地指定泛型格式 _ = MapKeys[int, string](m) lst := List[int]{} lst.Push(10) lst.Push(13) lst.Push(23) fmt.Println("list:", lst.GetAll()) }
- Go 中没有定义对映射键进行迭代的顺序,因此不同的调用可能会导致不同的顺序。
var m = map[int]string{1:"2",2:"4",4:"8"} fmt.Println("keys:", MapKeys(m)) // 返回的任务顺序都是有可能的 // keys: [4 1 2] | keys: [2 1 4]
error 错误
- 错误是最后一个返回值
- errors.New(“error”) 使用给定的错误消息构造一个基本值。
好像只看到在函数内使用
func f(arg int) (int, error) { if arg == 42 { return -1, errors.New("can't work with 42") } return arg + 3, nil }
- 标记错误,预先声明一个变量,
fmt.Errorf() 不是 errors.New()
var ErrOutOfTea = fmt.Errorf("no more tea available") var ErrPower = fmt.Errorf("can't boil water")
- 更高级别的错误包添加上下文,A 包装B,B包装C
var ErrPower = fmt.Errorf("can't boil water") // fmt.Errorf 包装了 ErrPower return fmt.Errorf("making tea: %w", ErrPower)
- rrors.Is检查给定错误(或其链中的任何错误)是否与特定错误值匹配。这对于包装或嵌套错误尤其有用,可让您识别错误链中的特定错误类型或标记错误。
有点像php instanceof
if errors.Is(err, ErrOutOfTea){ fmt.Println("We should buy new tea!") }
自定义错误
- 自定义错误已 Error 结尾
- 自定义错误需实现error 方法
// 自定义错误 type argError struct { arg int, message string } // 自定义错误实现Error方法 func (e *argError) Error() string { return fmt.Sprintf("%d - %s", e.arg, e.message) }
- 用自定义error 返回error
error 本质是个接口
func f(arg int) (int, error) { if arg == 42 { // error 本质是个接口, argError 实现了Error 方法,所以可以用来作为返回值 return -1, &argError{arg, "can't work with it"} } return arg + 3, nil }
- errors.As是更高级版本errors.Is。它检查给定的错误(或其链中的任何错误)是否与特定错误类型匹配,并转换为该类型的值,返回true。如果不匹配,则返回false。
func main() { _, err := f(42) var ae *argError // 判断err 是否是 ae 指针类型的错误,f()返回的就是指针类型的错误 if errors.As(err, &ae) { fmt.Println(ae.arg) fmt.Println(ae.message) } else { fmt.Println("err doesn't match argError") } }
goroutines 协程
- 匿名调用
go func(){}()
go func(msg string){ fmt.Println(msg) }("doing")
channels 渠道
- 通道是连接并发 goroutine 的管道。你可以将值从一个 goroutine 发送到通道,并在另一个 goroutine 中接收到这些值。
- 初始化渠道
messages := make(chan string)
- 默认情况下,发送和接收会阻塞,直到发送者和接收者都准备好为止
channel buffer 通道缓冲
- 默认情况下,通道是无缓冲的chan <-,这意味着,只有当有相应的接收 ( <- chan) 准备好接收发送的值时,它们才会接受发送 ( )。缓冲通道接受有限数量的值,而这些值没有相应的接收器。
- 创建有缓冲的通道
messages := make(chan string, 2)
channel-synchronization 通道同步
- 使用阻塞接收等待 goroutine 完成
func worker (done chan bool){ fmt.Println("working...") time.Sleep(time.Second) fmt.Println("done") done <- true } func main(){ // 生成带缓冲的通道 done := make(chan bool,1) // 通过协程去做任务 go worker(done) // 通过阻塞 等待协程任务完成 <- done }
通道方向(发送|接收)[往通道发送,从通道接收] channel-directions
- 只发送通道,参数限定
channeName chan <- string
func ping(pings chan<- string, msg string) { pings <- msg }
- 只接收通道, 参数限定
channeName <- chan string
func pong(pings <-chan string, pongs chan<- string) { msg := <-pings pongs <- msg }
select 选择
- select等待多个通道操作,本身是阻塞状态,所以称为等待
// 如果加了default,循环完后直接结束, case 是没有机会输出的 func main() { c1 := make(chan string) c2 := make(chan string) go func() { time.Sleep(1 * time.Second) c1 <- "one" }() go func() { time.Sleep(2 * time.Second) c2 <- "two" }() for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) //default: // time.Sleep(700 * time.Millisecond) // fmt.Println("default" ) } } }
timeoute 超时
- 通过time.After() 实现
- 直接<-time.After 在select 结构中,无需指定channel
// c1 是有缓冲的,在发送时不会收到阻塞,如果阻塞时间和任务执行时间差不多一样,就会出现超时 // 例:c2 执行时间需要2s, time.After 2 s, c2 结果timeout func main() { c1 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c1 <- "resutl 1" }() select { case res := <-c1: fmt.Println(res) case <-time.After(1 * time.Second): fmt.Println("timeout 1") } c2 := make(chan string, 1) go func() { time.Sleep(2 * time.Second) c2 <- "result 2" }() select { case res := <-c2: fmt.Println(res) case <-time.After(3 * time.Second): fmt.Println("timout 2") } }
非阻塞通道操作
- 通道无缓冲,select 直接走default
无缓冲的需要通道边发送,边接收
- select 只执行一次
func main() { messages := make(chan string) signals := make(chan bool) // 通道无缓冲,五值可取,直接走了default: no message received select { case msg := <-messages: fmt.Println("received message", msg) default: fmt.Println("no message received") } // 通道无缓冲,也没有接收器,发送不成功,也会走 default:no message received msg := "hi" select { case messages <- msg: fmt.Println("sent message", msg) default: fmt.Println("no message sent") } // 通道无缓冲-数据也接收不到 select { case msg := <-messages: fmt.Println("received message", msg) case sig := <-signals: fmt.Println("received signal", sig) default: fmt.Println("no activity") } } // no message received // no message sent // no activity
- 通过go 开启协程,实现边发送,边接收
func main() { message := make(chan string) signals := make(chan bool) go func() { message <- "1" }() go func() { // 利用阻塞等待模式-获取结果 msg := <-message fmt.Println(msg) signals <- true }() // 主进程阻塞等待 <-signals fmt.Println("over") }
- 利用select 无阻塞模式,发送和接收并行,并不一定能接收到
func main() { message := make(chan string) signals := make(chan bool) // 通过协程, 发送和接收并行 go func() { message <- "1" }() go func() { select { case msg := <-message: fmt.Println(msg) default: signals <- true } }() <-signals fmt.Println("over") } // 并行接收-直接走了 select 的default // over
关闭通道
- 可以从关闭的通道中读取,会立马读取到值,但是返回基础类型的零值, 第二个值是false
// close(), 关闭通道,仍然可以读取,返回 零值 & false, 用来判断通道是否还有数据 jobs := make(chan int, 5) close(jobs) last, ok := <-jobs fmt.Println("received more jobs:", last,ok) // 0 false
- 通过协程 for 一直从通道读取,判断通道是否有数据已经关闭
func main() { jobs := make(chan int, 5) done := make(chan bool) go func() { // 一直从通道读取, more 用来判断通道是否关闭 for { // j, more := <-jobs if more { fmt.Println("received job", j) } else { fmt.Println("received all jobs") done <- true return } } }() for j := 1; j <= 3; j++ { jobs <- j fmt.Println("sent job", j) } close(jobs) fmt.Println("sent all jobs") // 使用阻塞方式,来控制主进程关闭-保证协程处理完 <-done _, ok := <-jobs fmt.Println("received more jobs:", ok) }
- 通道不关闭,一直读取会死锁, 接收者第二个参数也用来判断channel 是否关闭
jobs := make(chan int, 5) done := make(chan bool) jobs <- 1 // 此处需要关闭,不关闭,会死锁,直到错误发生 close(jobs) for i := 0; i < 2; i++ { j, more := <-jobs fmt.Println("j:", j, "more:", more) }
- 通道有缓冲,无协程同步消耗,投递的任务量大于缓冲时,任务量过大时会被阻塞导致死锁
// 缓冲容量只有2,连续投放4个任务,会导致死锁产生 jobs := make(chan int, 2) jobs <- 1 jobs <- 2 jobs <- 3 jobs <- 4 close(jobs) for i := 0; i < 5; i++ { j, more := <-jobs fmt.Println("j:", j, "more:", more) }
- 主进程需要有阻塞,等待协程处理完,否则协程可能没机会执行完
func main() { jobs := make(chan int, 2) done := make(chan bool) // 依赖协程等待处理任务完成 go func() { for i := 0; i < 5; i++ { j, more := <-jobs fmt.Println("j:", j, "more:", more) } }() jobs <- 1 jobs <- 2 jobs <- 3 jobs <- 4 close(jobs) // close 放在这个位置, 协程会执行完,由time.Sleep // 通过time.Sleep 阻塞,验证协程是否有机会执行完 time.Sleep(time.Second * 1) // close 放在这个位置,协程由于读取阻塞,此处关闭,主进程执行完了,协程来不及处理,5次循环之执行了4次 close(jobs) }
range channel 对channel 进行迭代
- 直接通过range 迭代, 通道关闭后,仍然可以读取里面的值
queue := make(chan string, 2) queue <- "one" queue <- "two" close(queue) for elem := range queue { fmt.Println(elem) }
- 不close channel, 对其迭代,会造成死锁
queue := make(chan string, 2) queue <- "one" queue <- "two" // 此处注释了-关闭,会造成死锁 //close(queue) for elem := range queue { fmt.Println(elem) }
定时器(计时器) (某段时间后要做的事情)
- 计时器实例化
time.NewTimer(2 * time.Second)
- 计时器触发前取消 timer.Stop()
// 这里开启了1s的定时器,由协程处理 timer2 := time.NewTimer(time.Second) go func(){ <-timer2.C fmt.Println("Timer 2 fired") }() // 由于没有阻塞,不到1s就执行到这里了,可以在定时器执行之前取消 stop2 := timer2.Stop()
- 实例化一个定时器,主进程无需阻塞,由定时器channel 进行阻塞
func main(){ // 实例化一个定时器,2s 后触发 timer1 := time.NewTimer(2 * time.Second) // timer1.C 是一个chann 会阻塞在这 <-timer1.C fmt.Println("Timer 1 fired") // 实例化一个1s的定时器,由协程处理 timer2 := time.NewTimer(time.Second) go func(){ <-timer2.C fmt.Println("Timer 2 fired") }() // 协程处理,在定时器执行之前取消 stop2 := timer2.Stop() if stop2 { fmt.Println("Timer 2 stopped") } // 此处阻塞2s,只是为了给timer2 留出足够的时间验证定时器触发前取消 // 不阻塞的化,主进程直接执行完退出,定时器2没有机会执行完 time.Sleep(2 * time.Second) }
ticker (定期重复做某件事)
- ticker 周期性重复,定时器只执行一次,本质也是channel 周期投入值,配合for, select 使用
- ticker 实例
time.NewTicker()
- 计时器也是可停止的,
ticker.Stop()
func main(){ // 实例一个计时器 每 0.5s 一次 ticker := time.NewTicker(500 * time.Millisecond) done := make(chan bool) // 协程开启,等待处理计时器任务 go func(){ for { select { // 等待主进程标识,关闭协程, 终止 for??? case <-done: return case t :=<-ticker.C: fmt.Println("Tick at", t) } } }() // sleep 阻塞主进程,1.6s, 给协程执行时间的空间,按时间差计算, 协程的时间差 会执行3次 time.Sleep(1600 * time.Millisecond) // 计时器是可以停止的,(不停止,主进程执行完,协程也会关闭) ticker.Stop() // 控制协助的return(可能用来终止协程或销毁) done <-true fmt.Println("ticker stopped") }
woker-pool 工作池
- channel 未关闭的情况下,在协程里面里面range 会阻塞,但不会deadlock
- 主进程里面range channel,channel 一定要close 后;不然会deadlock
- range channel 是阻塞的
func worker(id int, jobs <-chan int, result chan<- int) { fmt.Println("worker ", id) // range channel 是阻塞的 for j := range jobs { fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) fmt.Println("worker", id, "finished job", j) result <- j * 2 } } func main() { const numJobs = 5 jobs := make(chan int, numJobs) result := make(chan int, numJobs) // 并发开启多个协程工作池 for w := 1; w <= 3; w++ { go worker(w, jobs, result) } for j := 1; j <= numJobs; j++ { jobs <- j } // 此处不关闭对协程中的range 不影响,协程里面阻塞等待读取 close(jobs) // 在主进程里面不关闭channel,进行读取,会死锁 //for j := range jobs { // fmt.Println("main started job", j) // time.Sleep(time.Second) // fmt.Println("main finished job", j) // result <- j * 2 //} for a := 1; a <= numJobs; a++ { // 通过channel 取值,阻塞主进程 // 此处没有close result channel, 但是指定了读取次数,会阻塞,且一定能拿到对应的次数的结果值,不然也会死锁 <-result } //fmt.Println("over") }
- 通过for 对
未关闭的
channel 进行迭代, 一定要明确所能取到的值,实现阻塞result := make(chan int, numJobs) // 这里明确reuslt channel 里面最少会有5个值的,能读取到,不会就会阻塞 deadlock 发生 for a := 1; a <= 5; a++ { <-result }
- 通过for 对
关闭
的 channel 进行迭代, 无任何阻塞result := make(chan int, numJobs) close(result) for a := 1; a <= 5; a++ { // 读取关闭的channel,无任何阻塞,协程有可能执行不完 <-result }
- 通道无缓冲的情况下,
协程
或主进程
投递任务阻塞时,会deadlock,开启和要投递的任务个数的同等协程消耗channel, 就不会阻塞报错func worker(id int, jobs <-chan int, result chan<- int) { fmt.Println("worker ", id) for j := range jobs { fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) fmt.Println("worker", id, "finished job", j) // 通过协程投递任务阻塞,deadlock result <- j * 2 } } func main() { const numJobs = 5 // 通道无缓冲 jobs := make(chan int) result := make(chan int) // for w := 1; w <= 3; w++ { go worker(w, jobs, result) } //time.Sleep(time.Second) for j := 1; j <= numJobs; j++ { // 主进程投递任务,也会阻塞, 等待协程消耗channel, 为要投递的任务释放位置 // 协程只有3个,处理每个任务要1s,明显会阻塞任务的投递 // 协程执行完释放,投第4个任务时, 没有消耗,会被堵死,deadlock(第4个任务无法投递成功,必须先开启消耗) // 协程开启和任务个数一样的,不会deadlock jobs <- j } close(jobs) for a := 1; a <= numJobs; a++ { <-result } }
- 无通道缓冲-协程持续消耗,最终等待所有任务投递完,主进程结束,
func worker(id int, jobs <-chan int) { fmt.Println("worker ", id) for { j := <-jobs fmt.Println("worker", id, "started job", j) time.Sleep(time.Second) fmt.Println("worker", id, "finished job", j) } } func main() { const numJobs = 5 jobs := make(chan int) // 无缓冲,只开启了一个协程,持续消耗channel go worker(1, jobs) for j := 1; j <= numJobs; j++ { // 投递任务会阻塞,第5个任务投递完主进程直接结束,协程里面第5个任务没有机会执行完 jobs <- j fmt.Println(j) } close(jobs) }
WaitGroups
- 将 WaitGroup 明确传递给函数,则应通过指针来完成。
!!!
- 使用sync包来处理
Wait, Add,Done
, 通过Wait 阻塞主进程,等协程执行完 - Done() 一次只能Done 一个, 不能Done成负数(会报错),Add 可以多个
- 计数器和协程个数不一致,主进程并不会等待协程处理完
// 在携程中处理这个任务 func worker(id int) { fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 5; i++ { // 开启和协程个数一样的计数器(为每个协程增加等待),主进程用来阻塞 wg.Add(1) go func() { //将 worker 调用包装在一个闭包中,以确保告知 WaitGroup 该 worker 已完成。这样,worker 本身就不必了解其执行中涉及的并发原语 defer wg.Done() worker(i) }() } // 阻塞主进程,直至计数器返回为0,保证每个协程都以完成任务 wg.Wait() }
速率限制
- time.Tick() 本质是个channel, 指定每多长时间后返回一个值(返回的是时间)和ticker的区别????)
- 直接 range tick;
要放在携程里面,放在主进程里面,永远执行不完,切不受close 限制
// time.Ticker 返回channel, 无限循环下去 for t := range time.Tick(200 * time.Millisecond) { burstyLimiter <- t }
- 通过设置chanel 类型是time.Time
make(chan time.Time)
, 通过定时器定时投放数据func main() { request := make(chan int, 5) for i := 1; i <= 5; i++ { request <- i } close(request) // 返回的是chan time.Time 类型的channel, 每200毫秒会投递一个任务进来 limiter := time.Tick(200 * time.Millisecond) for req := range request { // 通过channel 读取阻塞,没200ms 迭代一次 <-limiter fmt.Println("request", req, time.Now()) } //在速率限制方案中允许短时间突发请求,同时保持总体速率限制。我们可以通过缓冲限制器通道来实现这一点。此burstyLimiter 通道将允许最多 3 个事件的突发。 // 缓冲限制器,设置的为时间类型,方便后续从定时器里面去时间放进去 burstyLimiter := make(chan time.Time, 3) // 填满通道以表示允许突发。 // 有缓冲channel, 先把任务放满,读取时不会限制 for i := 0; i < 3; i++ { burstyLimiter <- time.Now() } // 通过协程 每200ms, 投递时间到缓冲限制器channel go func() { // 验证缓冲限制器阻塞问题 for t := range time.Tick(200 * time.Millisecond) { burstyLimiter <- t } }() //现在模拟另外 5 个传入请求。其中前 3 个将受益于 的突发功能burstyLimiter。 burstyRequests := make(chan int, 5) for i := 1; i <= 5; i++ { burstyRequests <- i } // 关闭后才能range close(burstyRequests) // 处理突发请求前3个, 后续没200ms处理一个,由协程tick 进行投递 for req := range burstyRequests { <-burstyLimiter fmt.Println("request", req, time.Now()) } }
- 定时器投放channel, 也会阻塞,不会报错
func main() { // 设置有缓冲的限制器(有无缓冲都不影响) //limit := make(chan time.Time, 1) limit := make(chan time.Time) // 先往缓冲器里面放一个值,是channel 处于阻塞状态 //limit <- time.Now() // 每1s消耗channel, go func() { for { time.Sleep(time.Second * 5) t := <-limit fmt.Println("go consumer:", t) } }() // 直接rang tick // 设定定时器,每200ms 投递一个任务,看阻塞是否报错 // 这个要放在协程里面才行,放在这里,会一直执行,也不会死锁 // 阻塞的情况下,步长等于阻塞时长 for tickT := range time.Tick(200 * time.Millisecond) { fmt.Println("投递:", tickT) limit <- tickT } // 主进程阻塞5s, 验证协程处理网(不会执行到这里来)-上面会持续执行tick fmt.Println("over start") time.Sleep(time.Second * 5) fmt.Println("over") }
原子计数器
- Go 中管理状态的主要机制是通过通道进行通信
- 原子计数器操作&获取
var ops atomic.Uint64 // 计数 ops.Add(1) // 安全的获取值 ops.Load()
- 原子计数器保证在多协程中的计数正确
sync/atomic
func main() { var ops atomic.Uint64 var wg sync.WaitGroup // 50个并发协程,进行原子计数,保证最终计数值正确 for i := 0; i < 50; i++ { wg.Add(1) // 并发写入“ops”,使用非原子计数有可能会得到不一样的值 go func() { for c := 0; c < 1000; c++ { ops.Add(1) } wg.Done() }() } wg.Wait() // 使用load 安全的读取,加载原子计数值 fmt.Println("ops:", ops.Load()) // 50000 }
- goroutine 会相互干扰。此外,在使用该标志运行时,我们会遇到数据争用失败 -race
互斥锁
- 原子操作来管理简单的计数器状态。对于更复杂的状态,我们可以使用互斥锁 来安全地跨多个 goroutine 访问数据。
- 互斥锁必须Lock ,Unlock
sync 包
// 互斥锁的零值可以按原样使用,因此这里不需要初始化。 var mu sync.Mutex mu.Lock() mu.Unlock()
- 容器保存计数器映射;由于我们想从多个 goroutine 同时更新它,因此我们添加了一个Mutex来同步访问。请注意,互斥锁不能被复制,因此如果 struct传递它,应该通过指针来完成
WaitGroup 同理
。 - 运行多个协程-操作同一块map key, 使用互斥保证安全
type Container struct { // 互斥锁 不需要初始化可以直接零值使用 mu sync.Mutex // 计数器容器 counters map[string]int } func (c *Container) inc(name string) { // 并发安全,先锁定,后解锁 // 不开启锁的话,由于协程是互斥的,并且同时并发请求,会造成数据竞争,直接报错了 c.mu.Lock() defer c.mu.Unlock() c.counters[name]++ } func main() { c := Container{ counters: map[string]int{"a": 0, "b": 0}, } var wg sync.WaitGroup // 匿名函数-协程并发处理 doIncrement := func(name string, n int) { for i := 0; i < n; i++ { c.inc(name) } wg.Done() } wg.Add(3) //运行多个 goroutine;注意它们都访问同一个Container,其中两个访问同一个计数器。 go doIncrement("a", 10000) go doIncrement("a", 10000) go doIncrement("b", 10000) wg.Wait() fmt.Println(c.counters) }
协程开启协程-是否受外层协程结束影响验证
- 协程里面开启的协程,不受外部协程是否执行完影响
- 主进程有足够的阻塞控制,任何地方开启的协程都会执行完
- 主进程等待时间足够长-协程里面的协程都会执行完(协程里面return的不会销毁,研究gmp 模型)
func main() { go func() { // 协程里面开启协程 go func() { //模拟第二层协程耗时任务5s, 观察第一层协程是否会等待 time.Sleep(time.Second * 5) fmt.Println("go2 finish") }() //模拟第一层协程耗时3s time.Sleep(time.Second * 3) fmt.Println("go1 finish") return }() // 此处验证主进程不阻塞等待的情况下,协程无法执行完 //time.Sleep(time.Second * 3) // 此处阻塞6s, 留出足够的时间(第一层和第二层协程最长时间),验证第一层协程执行完,第二层协程是否有机会执行 time.Sleep(time.Second * 6) fmt.Println("测试协程中开启协程 over") }
goroutines 协程的状态(待补)
sort 排序
- 参阅 cmp.Ordered,(https://pkg.go.dev/cmp#Ordered)
- 默认切片排序包,切片类型切片包,适用于任何 有序内置类型, 切片是否已经排序
slices.Sort(strs), slices.IsSorted()
// 对字符串进行排 strs := []string{"c", "a", "b"} slices.Sort(strs) // 对数字进行排序 ints := []int{7, 2, 4} slices.Sort(ints) // 检查切片是否已经按排序顺序排列。 s := slices.IsSorted(ints)
按功能排序(自定义排序方法)
- slice.SortFunc
fruits := []string{"peach", "banana", "kiwi"} // 自定义排序方法,通过字符串长度进行排序 lenCmp := func(a, b string) int { return cmp.Compare(len(a), len(b)) } slices.SortFunc(fruits, lenCmp) fmt.Println(fruits) type Person struct { name string age int } people := []Person{ {name: "Jax", age: 37}, {name: "TG", age: 25}, {name: "Alex", age: 72}, } slices.SortFunc(people, // 自定义排序方法,通过struct 类型里面的 age 进行排序 func(a, b Person) int { return cmp.Compare(a.age, b.age) }) fmt.Println(people)
- 如果Person结构很大,针对切片包含*Person并相应地调整排序函数
Panic 类似 throw new Exception
- 生成文件
os.Create( path)
返回文件句柄*os.File
- 通常panic表示发生了意外错误。我们主要用它来快速失败,以应对在正常操作期间不应发生的错误,或者我们无法妥善处理的错误。
- panic 的一个常见用途是,如果函数返回一个我们不知道如何处理(或不想处理)的错误值,则中止。
_, err := os.Create("/tmp/file") if err != nil { // 后面的代码不会执行 panic(err) } fmt.Println(1111)
- 将导致其崩溃,打印错误消息和 goroutine 跟踪,并以非零状态退出。
- 当第一次 panic inmain触发时,程序会退出而不执行其余代码。
- 在 Go 中,尽可能使用指示错误的返回值是惯用做法。
defer
- defer 先进后出
- Defer用于确保函数调用在程序执行的稍后阶段执行,通常用于清理目的。defer通常用于例如 ensure在finally其他语言中使用。
- 文件的创建,写入,关闭
// 生成 f, err := os.Create(p) // 写入 fmt.Fprintln(f, "data") // 关闭 f.Close()
recover
- recover panic 可以使用内置函数从 panic 中恢复
- recover必须在延迟函数内调用。当封闭函数发生恐慌时,延迟将激活,并且recover其中的调用将捕获恐慌。
defer func() { if r:= recover(); r != nil { fmt.Println("Recovered. Error:\n", r) } }()
- :如果某个客户端连接出现严重错误,服务器不会崩溃。相反,服务器会关闭该连接并继续为其他客户端提供服务。事实上,这正是 Gonet/http 默认为 HTTP 服务器所做的。
- 返回值recover是调用时引发的错误panic, 就是panic 抛出来的错误
字符串中常用的函数
- 用一个变量接收函数,短函数
var p = fmt.Println p("print")
- 常用字符串函数
import,expolde, repeat, replace
// 判断一个字符串中是否有子字符串 p("Contains: ", s.Contains("test", "es")) // 一个字符串串中出现多少次 p("count: ", s.Count("test","t")) // 是否有前缀 p("HasPrefix: ", s.HasPrefix("test", "te")) // 是否有后缀 p("HasSuffix:", s.HasSuffix("test","st")) // 在字符串中的索引位置 p("Index: ", s.Index("test","e")) // 拼接字符串 p("Join: ", s.Join([]string{"a","b","c"},"-")) // 重复字符串 p("Repeat: ", s.Repeat("a",5)) // 替换字符串 p("Replace: ",s.Replace("foo","o","0",1)) // 替换字符串 p("Replace: ",s.Replace("foo","o","0",-1)) // 分割字符串 p("Split: ", s.Split("a-b-c-d-e", "-")) // 字符串转小写 p("ToLower", s.ToLower("TEST")) // 字符串转大写 p("ToUpper", s.ToUpper("test"))
字符串格式化
- 常用函数
fmt.Printf()
,fmt.Sprintf()
,fmt.Fprintf()
- 格式说明,
%+v,%#v,%v,%T...
type point struct { x, y int } func main() { p := point{1, 2} fmt.Printf("struct1: %v\n", p) // %v 如果值是一个结构体,则%+v变体将包含该结构的字段名称。 fmt.Printf("struct2: %+v\n", p) // %#v 打印该值的 Go 语法表示,即可产生该值的源代码片段。 fmt.Printf("struct3: %#v\n", p) // %T 要打印值的类型,请使用%T。 fmt.Printf("type: %T\n", p) // %t 格式化布尔值很简单。 fmt.Printf("bool: %t\n", true) // %d 标准的十进制格式 fmt.Printf("int: %d\n", 123) // %b 打印二进制表示形式。 fmt.Printf("bin: %b\n", 14) // %c 打印与给定整数相对应的字符。 fmt.Printf("char: %c\n", 33) // %x 提供十六进制编码。 fmt.Printf("hex: %x\n", 456) // %f 浮点数也有多种格式化选项。对于基本十进制格式化,请使用%f。 fmt.Printf("float1: %f\n", 78.9) // %e 并%E以科学计数法(略有不同的版本)格式化浮点数。 fmt.Printf("float2: %e\n", 123400000.0) // %E 以科学计数法(略有不同的版本)格式化浮点数。 fmt.Printf("float3: %E\n", 123400000.0) // %s 对于基本字符串打印使用%s。 fmt.Printf("str1: %s\n", "\"string\"") // %q 要像在 Go 源中一样对字符串进行双引号处理,请使用%q。 fmt.Printf("str2: %q\n", "\"string\"") // %x 以 16 为基数呈现字符串,每个输入字节有两个输出字符。 fmt.Printf("str2: %x\n", "hex this") // %p 要打印指针的表示形式,请使用%p。 fmt.Printf("pinter:: %p\n", &p) // %6d 在格式化数字时,您经常需要控制结果数字的宽度和精度。要指定整数的宽度,请在%动词后使用数字。默认情况下,结果将右对齐并用空格填充。 fmt.Printf("width1: |%6d|%6d|\n", 1.2, 3.45) // %6.2f 指定打印浮点数的宽度,但通常您还需要同时使用 width.precision 语法来限制小数精度。 fmt.Printf("width2: |%6.2f|%6.2f|\n", 1.2, 3.45) // %-6.2f 要左对齐,请使用-标志。 fmt.Printf("width3: |%-6.2f|%-6.2f|\n", 1.2, 3.45) // %6s 右对齐 格式化字符串时控制宽度,尤其是为了确保它们在类似表格的输出中对齐。对于基本的右对齐宽度 fmt.Printf("width4: |%6s|%6s|\n", "foo", "b") // %-6s 左对齐,请使用-与数字相同的标志。 fmt.Printf("width5: |%-6s|%-6s|\n", "foo", "b") // Printf,它将格式化的字符串打印到os.Stdout。Sprintf格式并返回一个字符串而不在任何地方打印它。 s := fmt.Sprintf("sprintf: a %s", "string") fmt.Println(s) // io.Writers您可以使用除 os.Stdout使用之外的其他格式+打印Fprintf。 fmt.Fprintf(os.Stderr, "io; an %s\n", "error") }
文本模板 text.Template
- 用text/template包创建动态内容或向用户显示自定义输出
template.New("t1")
,Parse
,Must
,Execute
func main() { /** 我们可以创建一个新模板并从字符串中解析其主体。模板是静态文本和其中包含的“操作”的混合体, {{...}}用于动态插入内容。 */ t1 := template.New("t1") t1, err := t1.Parse("Value is {{.}}\n") if err != nil { panic(err) } /* 使用函数来在返回错误template.Must时 panic 。这对于在全局范围内初始化的模板特别有用。Parse */ t1 = template.Must(t1.Parse("Value: {{.}}\n")) /* 通过“执行”模板,我们生成了其文本,其中包含其操作的具体值。{{.}}操作被作为参数传递给 的值替换Execute。 */ t1.Execute(os.Stdout, "some text") t1.Execute(os.Stdout, 5) t1.Execute(os.Stdout, []string{ "Go", "Rust", "C++", "C#", }) // 使用辅助函数 Create := func(name, t string) *template.Template { return template.Must(template.New(name).Parse(t)) } //如果数据是结构体,我们可以使用{{.FieldName}}操作来访问其字段。模板执行时,字段应导出以便访问。 t2 := Create("t2", "Name: {{.Name}}\n") t2.Execute(os.Stdout, struct { Name string }{"Jane Doe"}) //同样适用于地图;地图对键名称的大小写没有限制 t2.Execute(os.Stdout, map[string]string{ "Name": "Mickey Mouse", }) //if/else 为模板提供条件执行。如果某个值是某个类型的默认值(例如 0、空字符串、nil 指针等),则该值被视为 false。此示例演示了模板的另一个功能:使用-操作来修剪空格。 t3 := Create("t3", "{{if . -}} yes {{else -}} no {{end}}\n") t3.Execute(os.Stdout, "not empty") t3.Execute(os.Stdout, "") // 范围块让我们可以循环遍历切片、数组、映射或通道。范围块内部{{.}}设置为迭代的当前项。 t4 := Create("t4", "Range: {{range .}}{{.}} {{end}}\n") t4.Execute(os.Stdout, []string{ "Go", "Rust", "C++", "C#", }) }
正则表达式(待实践)
Json
- 内置支持
encoding/json
- 编码&解码
json.Marshal()
,json.Unmarshal()
, 解码会返回错误 - 自定义数据结构 struct, 可以设置哪些字段是可导出的,首字母大小写区分
json.NewEncoder(os.Stdout)
将 JSON 编码直接流式传输到os.Writers 类 os.Stdout或甚至 HTTP 响应主体。enc := json.NewEncoder(os.Stdout) d := map[string]int{"apple": 5, "lettuce": 7} enc.Encode(d)
- 将json字符传转为byte切片,才能反解码
str := `{"page":1, "fruits":["apple","peach"]}` res := response2{} // 将原json格式转为[]byte json.Unmarshal([]byte(str), &res)
- 对map,或解码中的值进行类型转换
data[key].(float64)
byt := []byte(`{"num":6.13, "strs":["a","b"]}`) var dat map[string]interface{} json.Unmarshal(byt, &dat); num := dat["num"].(float64)
- struct 类型,自定义类型的编码和解码,设置标签
json:"page"
type response1 struct { Page int Fruits []string } // 只有导出的字段才会以 JSON 格式进行编码/解码。字段必须以大写字母开头才能导出。 type response2 struct { Page int `json:"page"` Fruits []string `json:"fruits"` }
- 转换基本数据类型 bool,int,str
// 直接bool bolB, _ := json.Marshal(true) fmt.Println(string(bolB)) // 直接int intB, _ := json.Marshal(1) fmt.Println(string(intB)) // 直接float fltB, _ := json.Marshal(2.34) fmt.Println(string(fltB)) // 直接string strB, _ := json.Marshal("gopher") fmt.Println(string(strB))
- 转换复杂类型,map, slice, struct
// 直接转换切片,类似php中的 ["a","b"] slcD := []string{"apple", "peach", "pear"} slcB, _ := json.Marshal(slcD) fmt.Println(string(slcB)) // 转换map 类型 mapD := map[string]int{"apple": 5, "lettuce": 7} mapB, _ := json.Marshal(mapD) fmt.Println(string(mapB)) // 转换结构体 // JSON 包可以自动编码您的自定义数据类型。它将只在编码输出中包含导出的字段,并默认使用这些名称作为 JSON 键。只有大写的才会导出 res1D := &response1{ Page: 1, Fruits: []string{"apple", "peach", "pear"}, } res1B, _ := json.Marshal(res1D) fmt.Println(string(res1B)) // 转换结构体 - 结构体中设置的标签生效 res2D := &response2{ Page: 1, Fruits: []string{"apple", "peach", "pear"}, } res2B, _ := json.Marshal(res2D) fmt.Println(string(res2B))
- 反解码,将json格式字符串转换为map, struct
// json字符串 处理成 byte 切片,转换为 map // [123 34 110 117 109 34 58 54 46 49 51 44 32 34 115 116 114 115 34 58 91 34 97 34 44 34 98 34 93 125] // 将 JSON 数据解码为 Go 值。这是一个通用数据结构的示例。 byt := []byte(`{"num":6.13, "strs":["a","b"]}`) // 提供一个变量,JSON 包可以将解码的数据放入该变量中。这 map[string]interface{}将保存字符串到任意数据类型的映射。 var dat map[string]interface{} // 解码导入到变量中 if err := json.Unmarshal(byt, &dat); err != nil { panic(err) } fmt.Println(dat) //使用解码映射中的值,我们需要将它们转换为适当的类型。例如,我们将其中的值转换num为预期的float64类型。 num := dat["num"].(float64) fmt.Println(num) // 访问嵌套数据需要一系列转换。 // 1. 先转换为切片interface strs := dat["strs"].([]interface{}) // 2. 再将切片里面的值转换为string str1 := strs[0].(string) fmt.Println(str1) // 将 JSON 解码为自定义数据类型。这样做的好处是可以为我们的程序增加额外的类型安全性,并且在访问解码的数据时无需进行类型断言。 str := `{"page":1, "fruits":["apple","peach"]}` res := response2{} json.Unmarshal([]byte(str), &res) fmt.Println(res) fmt.Println(res.Fruits[0]) // 将 JSON 编码直接流式传输到os.Writers 类 os.Stdout或甚至 HTTP 响应主体。 enc := json.NewEncoder(os.Stdout) d := map[string]int{"apple": 5, "lettuce": 7} enc.Encode(d)
xml 待实践
time
- 获取当前时间
now := time.Now() // 2024-09-02 16:32:35.565605 +0800 CST m=+0.000123188
- 构建时间
time.Date()
;提供年、月、日等来构建结构。时间总是与Location时区相关联then := time.Date( 2009, 11, 17, 20, 34, 58, 651387237, time.UTC, ) // 2009-11-17 20:34:58.651387237 +0000 UTC
- 预期提取时间值的各个组成部分。
p(then.Year()) p(then.Month()) p(then.Day()) p(then.Hour()) p(then.Minute()) p(then.Second()) p(then.Nanosecond()) // 提取纳秒 p(then.Location()) // 提取时区 p(then.Weekday()) // 提取日期对应的是周几
- 日期比较(在前,在后, 相等), 返回bool
// now 当前时间 // then 构造出来的时间 通过time.Date p(then.Before(now)) p(then.After(now)) p(then.Equal(now))
- 日期差值计算
time.Sub()
Sub方法返回Duration表示两个时间之间的间隔。// now 比then大多少,时间小的在前,会出现负数 diff := now.Sub(then) p(diff) // 129660h12m45.985863763s p(diff.Hours()) // 差了多少小时 返回科学记数法 p(diff.Minutes()) // 差了多少分钟 p(diff.Seconds()) // 差了多少秒 p(diff.Nanoseconds()) // 差了多少纳秒
- 日期加时长
time.Add()
,使用Add将时间提前指定的持续时间,或使用-将时间向后移动指定的持续时间。diff := now.Sub(then) p(then.Add(diff)) p(then.Add(-diff))
Epoch
- 程序中常见的需求是获取自Unix 纪元以来的秒数、毫秒数或纳秒数
- 获取秒级时间戳,毫秒级时间戳,纳秒级时间戳
now := time.Now() // 2024-09-02 17:26:41.110632 +0800 CST m=+0.000163345 fmt.Println(now) fmt.Println(now.Unix()) // 秒级 1725269201 fmt.Println(now.UnixMilli()) // 毫秒级 1725269201110 fmt.Println(now.UnixMicro()) // 纳秒级 1725269201110632
- 将自纪元以来的整数秒数或纳秒数转换为相应的time
// 将时间戳 转为秒级时间 fmt.Println(time.Unix(now.Unix(), 0)) // 2024-09-02 17:26:41 +0800 CST // 将时间戳 转为毫秒级时间 fmt.Println(time.Unix(0, now.UnixNano())) // 2024-09-02 17:26:41.110632 +0800 CST
时间格式化
- time.Parse 解析时间, 时间返回值 格式 t.Format()
func main() { p := fmt.Println // 根据 RFC3339 使用相应的布局常量格式化时间的基本 t := time.Now() p(t.Format(time.RFC3339)) // 2024-09-02T18:05:46+08:00 // 时间解析使用与相同的布局值Format。 t1, e := time.Parse( time.RFC3339, "2012-11-01 22:08:41+00:00", ) p(t1) /* Format并Parse使用基于示例的布局。通常,您会为这些布局使用常量time,但您也可以提供自定义布局。布局必须使用参考时间Mon Jan 2 15:04:05 MST 2006来显示格式化/解析给定时间/字符串的模式。示例时间必须与显示的完全一致:2006 年、15 表示小时、星期一表示星期几等。 */ p(t.Format("3:04PM")) p(t.Format("Mon Jan _2 15:04:05 2006")) p(t.Format("2006-01-02T15:04:05.999999-07:00")) form := "3 04 PM" t2, e := time.Parse(form, "8 41 PM") p(t2) // 对于纯数字表示,您还可以将标准字符串格式与时间值的提取部分一起使用。 fmt.Printf("%d-%02d-%02dT%02d:%02d:%02d-00:00\n", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), ) // Parse将返回有关格式错误的输入的错误,解释解析问题。 ansic := "Mon Jan _2 15:04:05 2006" _, e = time.Parse(ansic, "8:41PM") p(e) }
随机数
- IntN 在v2 版本里面
math/rand/v2
, 生成一个区间随机数// rand.IntN返回一个随机的intn 0 <= n < 100 fmt.Print(rand.IntN(100), ",") fmt.Print(rand.IntN(100))
- 生成种子
rand.NewPCG
// 生成一个种子,NewPCG创建一个 需要两个数字的种子的新PCGuint64源 s2 := rand.NewPCG(42, 1024) // 实例化一个随机对象,把种子放进去 r2 := rand.New(s2) fmt.Println(r2.IntN(100), ",") fmt.Println(r2.IntN(100)) fmt.Println()
- 生成一个浮点随机数
rand.Float64()
// rand.Float64返回一个float64 f, 0.0 <= f < 1.0。 // 这可用于生成其他范围的随机浮点数,例如5.0 <= f' < 10.0。 fmt.Print((rand.Float64()*5)+5, ",") fmt.Print((rand.Float64() * 5) + 5) fmt.Println()
数字解析 (将字符串数字解析成数字, strconv)
strconv.ParseFloat()
,strconv.ParseInt()
,strconv.ParseUint()
// 需要解析多少位精度 f, _ := strconv.ParseFloat("1.234", 64) fmt.Println(f) // ParseInt,0意味着从字符串推断基数。64要求结果适合 64 位。 i, _ := strconv.ParseInt("123", 0, 64) fmt.Println(i) // ParseInt将识别十六进制格式的数字。 d, _ := strconv.ParseInt("0x1c8", 0, 64) fmt.Println(d) // 无符号 u, _ := strconv.ParseUint("789", 0, 64) fmt.Println(u) //Atoi是一个用于基本十进制 int解析的便捷函数。 k, _ := strconv.Atoi("135") fmt.Println(k) _, e := strconv.Atoi("wat") fmt.Println(e)
Url 解析url.Parse(s)
,net/url
- url 分割
net.SplitHostPort(u.Host)
func main() { s := "postgres://user:pass@host.com:5432/path?k=v#f" // 解析url u, err := url.Parse(s) if err != nil { panic(err) } fmt.Println(u.Scheme) // User包含所有身份验证信息;调用 Username并Password获取单独的值。 fmt.Println(u.User) fmt.Println(u.User.Username()) p, _ := u.User.Password() fmt.Println(p) // 包含Host主机名和端口(如果存在)。用于SplitHostPort提取它们。 fmt.Println(u.Host) host, port, _ := net.SplitHostPort(u.Host) fmt.Println(host) fmt.Println(port) // 这里我们提取了path和 之后的片段#。 fmt.Println(u.Path) fmt.Println(u.Fragment) // 要以格式的字符串获取查询参数k=v,请使用RawQuery。您还可以将查询参数解析为映射。解析后的查询参数映射是从字符串到字符串切片,因此[0] 如果您只想要第一个值,请索引到。 fmt.Println(u.RawQuery) m, _ := url.ParseQuery(u.RawQuery) fmt.Println(m) fmt.Println(m["k"][0]) }
Sha256 哈希值 crypto\sha256
- SHA256 哈希值通常用于计算二进制或文本 blob 的短标识。例如,TLS/SSL 证书使用 SHA256 来计算证书的签名。
- 加密包
crypto\*
- 生成新的hash
h := sha256.New()
- hash 计算
[]byte(string)
Write需要字节。如果您有一个字符串s,请使用[]byte(s)将其强制转换为字节。s:= "this is string" h := sha256.New() h.Write([]byte(s)) // 取结果 bs := h.Sum(nil)
base64 编码encoding/base64
- 字符串编码&解码
b64.StdEncoding.EncodeToString([]byte(data))
,b64.StdEncoding.DecodeString(sEnc)
// 编码需要将字符串转换为 []byte() sEnc := b64.StdEncoding.EncodeToString([]byte(data)) fmt.Println(sEnc) // 解码需要将返回的[]byte 转换为 字符串string([]byte) sDec, _ := b64.StdEncoding.DecodeString(sEnc) fmt.Println(sDec) fmt.Println(string(sDec))
- Go 支持标准和 URL 兼容的 base64。
- Url兼容的 base64 格式进行编码/解码。
b64.URLEncoding.EncodeToString([]byte(data))
,b64.URLEncoding.DecodeString(uEnc)
uEnc := b64.URLEncoding.EncodeToString([]byte(data)) fmt.Println(uEnc) uDec, _ := b64.URLEncoding.DecodeString(uEnc) fmt.Println(string(uDec))
读取文件os
,io
,bufio
- 读取文件
os.ReadFile(path)
- 打开文件
os.Open("/tmp/dat")
io.ReadAtLeast(f, b3, 2)
更稳健地实现上述读取ReadAtLeast- 打开文件,将文件读取到[]byte中
b1 := make([]byte, 5) n1, err := f.Read(b1)
- bufio 实现了一个缓冲读取器,它既可以提高许多小读取的效率,又可以提供额外的读取方法。
bufio 包
,bufio.NewReader(f)
- 文件读取游标
io.SeekStart
,io.SeekCurrent
,io.SeekEnd
- 文件关闭
f.Close()
// 将文件的全部内容读取到内存中 dat, err := os.ReadFile("/tmp/dat") check(err) fmt.Println(string(dat)) // 更好地控制如何读取文件以及读取文件的哪些部分。对于这些任务,首先要Open读取文件以获取os.File值。 // 先打开文件,自由控制 f, err := os.Open("/tmp/dat") check(err) // 文件开头读取一些字节。最多允许读取 5 个字节, 实际会读完第一行 b1 := make([]byte, 5) n1, err := f.Read(b1) check(err) fmt.Printf("%d bytes: %s\n", n1, string(b1[:n1])) // 设置偏移量,从Seek文件中的已知位置进行操作。Read o2, err := f.Seek(6, io.SeekStart) check(err) b2 := make([]byte, 2) n2, err := f.Read(b2) check(err) fmt.Printf("%d bytes @ %d: \n", n2, o2) // 相对位置的设置 _, err = f.Seek(4, io.SeekCurrent) check(err) // 相对末尾的位置设置 _, err = f.Seek(-10, io.SeekEnd) check(err) o3, err := f.Seek(6, io.SeekStart) check(err) b3 := make([]byte, 2) // 该io包提供了一些可能有助于文件读取的函数。例如,可以使用 更稳健地实现上述读取ReadAtLeast n3, err := io.ReadAtLeast(f, b3, 2) check(err) fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3)) // 通过从开始设置负值,实现倒带;虽然没有内置倒带功能,但 Seek(0, io.SeekStart)可以实现这一点。 _, err = f.Seek(0, io.SeekStart) check(err) //该bufio包实现了一个缓冲读取器,它既可以提高许多小读取的效率,又可以提供额外的读取方法。 r4 := bufio.NewReader(f) b4, err := r4.Peek(5) check(err) fmt.Printf("5 bytes: %s\n", string(b4)) // 关闭文件,可以痛殴 defer f.Close() f.Close()
写入文件 os.Create()
,os.WriteFile()
,f.Write
- 直接写入,指定写入权限,返回是否有错误
- 通过创建文件写入,返回写入的长度&错误
f.Write()
- 直接写入字符串,
f.WriteString()
- 稳定写入,
f.Sync()
- 通过写缓冲器写入
bufio.NewWriter(f)
,写完之后刷新缓冲器w.Flush()
,确认所有缓冲都已写入// 字符串转字节直接写入 d1 := []byte("hello \ngo\n") err := os.WriteFile("/tmp/dat1", d1, 0644) check(err) // 通过打开文件的方式写入 f, err := os.Create("/tmp/dat2") check(err) defer f.Close() // 字符串 切片 ansii 码 d2 := []byte{115, 111, 109, 101, 10} n2, err := f.Write(d2) fmt.Printf("wrote %d bytes\n", n2) // 直接写入字符串 n3, err := f.WriteString("writes\n") check(err) fmt.Printf("wrote %d bytes\n", n3) //发出一个命令Sync来刷新写入稳定存储。 f.Sync() // bufio除了我们之前看到的缓冲读取器之外,还提供了缓冲写入器。 w := bufio.NewWriter(f) // 直接写入字符串 n4, err := w.WriteString("buffered\n") check(err) fmt.Printf("wrote %d bytes\n", n4) //用于Flush确保所有缓冲操作都已应用于底层写入器。 w.Flush()
线路过滤器(针对标准输入输出的处理)os.Stderr
,os.Stdin
,so.Stdout
- (它读取标准输入上的输入,对其进行处理,然后将一些派生结果打印到标准输出。grep是sed常见的行过滤器。)
- 使用缓冲扫描器,扫描标准输入
func main() { //使用缓冲扫描仪包装os.Stdin非缓冲扫描仪为我们提供了一种方便的Scan方法,可以使扫描仪前进到下一个标记;这是默认扫描仪中的下一行。 scanner := bufio.NewScanner(os.Stdin) // Scan返回的是个bool, 相对于while() for scanner.Scan() { //Text从输入中返回当前标记,这里是下一行。 ucl := strings.ToUpper(scanner.Text()) fmt.Println(ucl) } //检查过程中的错误Scan。预期文件结束但未将其报告Scan为错误。 if err := scanner.Err(); err != nil { fmt.Println(os.Stderr, "error:", err) os.Exit(1) } }
- 执行,将标准输入通过管道方式进行处理
$ echo 'hello' > /tmp/lines $ echo 'filter' >> /tmp/lines ## 通过管道的方式,用过滤器接收文件读取的输出 $ cat /tmp/lines | go run line-filters.go
文件目录(path包)path/filepath
- 构造规范化的路径
filepath.Join()
- 区分dir 和base
filepath.Base()
,filepath.Dir()
,拆分目录和文件 - 区分是绝对路径还是相对路径
filepath.IsAbs()
- 读取文件扩展名
filepath.Ext()
- 删除后缀
strings.TrimSuffix(filename, ext)
- 相对路径查找
filepath.Rel("a/b", "a/b/t/file")
// Join应该用来以可移植的方式构造路径。它接受任意数量的参数并从中构造层次路径 p := filepath.Join("dir1", "dir2", "filename") // dir1/dir2/filename fmt.Println("p:", p) // 使用Join而不是手动连接/s 或\s。除了提供可移植性之外,Join还将通过删除多余的分隔符和目录更改来规范化路径。 fmt.Println(filepath.Join("dir1//", "filename")) // dir1/filename fmt.Println(filepath.Join("dir1/../dir1", "filename")) // dir1/filename //Dir&Base用于将路径拆分为目录和文件。或者,Split将在同一调用中返回两者 fmt.Println("Dir(p):", filepath.Dir(p)) // dir1/dir2 fmt.Println("Base(p)", filepath.Base(p)) // filename // 检查路径是否是绝对路径。 fmt.Println(filepath.IsAbs("dir/file")) // false fmt.Println(filepath.IsAbs("/dir/file")) // true filename := "config.json" // 文件名的扩展名跟在点后面。我们可以用 来将扩展名从这些名称中分离出来Ext ext := filepath.Ext(filename) // .json fmt.Println(ext) // 要查找删除扩展名后的文件名,使用strings.TrimSuffix。 fmt.Println(strings.TrimSuffix(filename, ext)) // config // Rel查找base和 target之间的相对路径。如果 target 不能相对于 base ,则返回错误。 rel, err := filepath.Rel("a/b", "a/b/t/file") // t/file if err != nil { panic(err) } fmt.Println(rel) rel, err = filepath.Rel("a/b", "a/c/t/file") // ../c/t/file if err != nil { panic(err) } fmt.Println(rel)
目录 os
,io/fs
,path/filepath
- 生成目录
os.Mkdir("subdir", 0755)
- 删除目录
os.RemoveAll("subdir")
, 类似 rm -rf - 生成多层级目录
os.MkdirAll("subdir/parent/child", 0755)
- 读取目录
os.ReadDir("subdir/parent")
需要遍历 - 切换目录,
os.Chdir("subdir/parent/child")
,等同于cd - 目录列表读取名字,是否是目录
entry.Name()
,entry.IsDir()
- 递归访问目录,通过callback
filepath.WalkDir("subdir", visit)
func main() { // 在当前目录中创建一子目录 err := os.Mkdir("subdir", 0755) check(err) // 创建临时目录时,最好将defer其删除。os.RemoveAll 将删除整个目录树(类似于 rm -rf)。 defer os.RemoveAll("subdir") // 用于创建新空文件的辅助函数。 createEmptyFile := func(name string) { d := []byte("") check(os.WriteFile(name, d, 0644)) } createEmptyFile("subdir/file1") // 创建目录层次结构,包括父级MkdirAll。这类似于命令行mkdir -p。 err = os.MkdirAll("subdir/parent/child", 0755) check(err) createEmptyFile("subdir/parent/file2") createEmptyFile("subdir/parent/file3") createEmptyFile("subdir/parent/child/file4") // 读取目录,返回对象切片os.DirEntry , 返回的是个切片 c, err := os.ReadDir("subdir/parent") check(err) fmt.Println("Listing subdir/parent") // 遍历切片,丢弃数字索引 for _, entry := range c { // 获取目录名字,以及是否是目录 fmt.Println(" ", entry.Name(), entry.IsDir()) } // Chdir让我们改变当前工作目录,类似于cd。 // 切换到指定目录 err = os.Chdir("subdir/parent/child") check(err) // 读取切换的后的目录下的内容(列表) // subdir/parent/child 现在我们将看到列出当前目录时的内容。 c, err = os.ReadDir(".") check(err) fmt.Println("Listing subidr/parent/child") for _, entry := range c { fmt.Println(" ", entry.Name(), entry.IsDir()) } // 回到当前目录 err = os.Chdir("../../..") check(err) fmt.Println("Visiting subdir") // 递归访问目录,包括其所有子目录。WalkDir接受回调函数来处理访问的每个文件或目录 // 自定义闭包,递归读取文件目录 err = filepath.WalkDir("subdir", visit) } // visit对 递归 找到的每个文件或目录都调用filepath.WalkDir。 func visit(path string, d fs.DirEntry, err error) error { if err != nil { return err } fmt.Println(" ", path, d.IsDir()) return nil }
零时文件和目录
- 我们经常需要创建一些程序退出后不再需要的数据。 临时文件和目录非常适合于此目的,因为它们不会随着时间的推移污染文件系统。
- 创建临时文件
os.CreateTemp("", "sample")
, 第一个参数为空,在默认临时目录区,第二个参数给出临时文件名前缀 - 返回临时文件名,
f.Name()
- 删除临时文件
os.Remove(f.Name())
, - 写入临时文件
f.Write()
- 创建临时目录
os.MkdirTemp("", "sampledir")
, 第二个参数代表目录前缀 - 直接写入文件
os.WriteFile(fname, []byte{1, 2}, 0666)
f.Write()
和os.WriteFile()
的区别,f.Write 是针对打开的文件,os.WriteFiel 打开并写入// 创建临时文件的最简单方法是调用os.CreateTemp。它会创建一个文件并将 其打开以供读写。我们将 用作"" 第一个参数,因此os.CreateTemp将在操作系统的默认位置创建该文件。 f, err := os.CreateTemp("", "sample") check(err) // 显示临时文件的名称。在基于 Unix 的操作系统上,目录可能是/tmp。文件名以作为第二个参数给出的前缀开头,os.CreateTemp其余部分自动选择,以确保并发调用始终会创建不同的文件名。 fmt.Println("Temp file name:", f.Name()) // 完成后清理文件。操作系统可能会在一段时间后自行清理临时文件,但明确执行此操作是一种很好的做法 defer os.Remove(f.Name()) // 将数据写入临时文件 _, err = f.Write([]byte{1, 2, 3, 4}) check(err) // 写入许多临时文件,我们可能更愿意创建一个临时目录。 os.MkdirTemp的参数与 的相同 CreateTemp,但它返回一个目录名 而不是打开的文件 dname, err := os.MkdirTemp("", "sampledir") check(err) fmt.Println("Temp dir name:", dname) defer os.RemoveAll(dname) // 在临时文件名前加上临时目录来合成临时文件名。 fname := filepath.Join(dname, "file1") err = os.WriteFile(fname, []byte{1, 2}, 0666) check(err)
嵌入指令embed
包
- go:embed是一个编译器指令,允许程序在构建时在 Go 二进制文件中包括任意文件和文件夹
- 注释下的嵌入指令是必须的
//go:embed folder/single_file.txt
,该注释必须!!!//go:embed folder/single_file.txt var fileString string //go:embed folder/single_file.txt var fileByte []byte //go:embed folder/single_file.txt //go:embed folder/*.hash var folder embed.FS
embed.FS
使用通配符嵌入多个文件甚至文件夹。这使用embed.FS 类型的变量,它实现了一个简单的虚拟文件系统。embed.FS
嵌入的虚拟文件系统指定了文件或者匹配模式,可以指定文件名进行读取- embed指令接受相对于包含 Go 源文件的目录的路径。此指令将文件的内容嵌入到 string紧随其后的变量中。
嵌入中追加字符,会自动换行
import ( "embed" ) /* 嵌入的注释指令必须存在 */ // embed指令接受相对于包含 Go 源文件的目录的路径。此指令将文件的内容嵌入到 string紧随其后的变量中。 //go:embed folder/single_file.txt var fileString string // 将文件内容嵌入到[]byte. //go:embed folder/single_file.txt var fileByte []byte // 使用通配符嵌入多个文件甚至文件夹。这使用embed.FS 类型的变量,它实现了一个简单的虚拟文件系统。 //go:embed folder/single_file.txt //go:embed folder/*.hash var folder embed.FS func main() { /* 此处变量未负值,通过嵌入,将文件类容赋值给了变量 fileString, fileByte,可以显示打印结果 */ // 打印出的内容single_file.txt。 fmt.Println(fileString, "=====") // hello go fmt.Println(string(fileByte), "======") // hello go //print(fileString) //print(string(fileByte)) /* 上面写了通配符,此处直接在嵌入文件系统中读取匹配的文件类容,指定文件名读取类容 */ // 从嵌入文件夹中检索一些文件。 content1, _ := folder.ReadFile("folder/file1.hash") fmt.Println(string(content1), "----read-file") // ======123 (追加了字符,嵌入顺序被打乱了) content2, _ := folder.ReadFile("folder/file2.hash") fmt.Println(string(content2), "---read-file") }
- 预先准备好文件内容,验证嵌入
使用这些命令来运行示例。(注意:由于 go Play 的限制,此示例只能在本地机器上运行。)
$ mkdir -p folder $ echo "hello go" > folder/single_file.txt $ echo "123" > folder/file1.hash $ echo "456" > folder/file2.hash $ go run embed-directive.go hello go hello go 123 456
- 实际执行结果
进行了自动换行
hello go ===== hello go ====== 123 ----read-file 456 ---read-file
测试&基准测试(基准:Benchmark开头-循环多次执行一个函数, 测试:Test 开头)
- 需要在项目目录下,就包含go.mod 文件的目录下
- 测试函数名称以
Test
开头 - 测试文件以
_test
结尾 - t.Error将报告测试失败但继续执行测试。t.Fatal将报告测试失败并立即停止测试。
- 基准测试函数名称以
Benchmark
开头func IntMin(a, b int) int { if a < b { return a } return b } func TestIntMinBasic(t *testing.T) { ans := IntMin(2, -2) if ans != -2 { // Errorf 报告错误,继续执行 t.Errorf("IntMin(2,-2) = %d; want -2", ans) } } func TestIntMinTableDriven(t *testing.T) { var tests = []struct { a, b int want int }{ {0, 1, 0}, {1, 0, 0}, {2, -2, -2}, {0, -1, -1}, {-1, 0, -1}, } for _, tt := range tests { testname := fmt.Sprintf("%d, %d", tt.a, tt.b) t.Run(testname, func(t *testing.T) { ans := IntMin(tt.a, tt.b) if ans != tt.want { // 参数T 打印错误 t.Errorf("got %d, want %d", ans, tt.want) } }) } } // 基准测试通常以_test.go文件形式进行,并以 开头命名Benchmark。testing运行器会多次执行每个基准测试函数, b.N每次运行都会增加次数,直到收集到精确的测量结果。 func BenchmarkIntMin(b *testing.B) { // 基准测试会循环运行我们正在测试的函数b.N。 for i := 0; i < b.N; i++ { IntMin(1, 2) } }
- 测试命令
go test -v
- 基准测试命令
go test -bench=.
//运行当前项目中的所有基准测试。所有测试均在基准测试之前运行。该bench标志使用正则表达式过滤基准测试函数名称。
命令行参数 os.Args
- 参数以slice 类型,第一个参数是 路径
- 需要先编译成命令,执行加入参数
func main() { // os.Args提供对原始命令行参数的访问。请注意,此切片中的第一个值是程序的路径,并os.Args[1:] 保存程序的参数。 argsWithProg := os.Args argsWithOutProg := os.Args[1:] // 可以通过正常索引获取单独的参数。 arg := os.Args[3] fmt.Println(argsWithProg) fmt.Println(argsWithOutProg) fmt.Println(arg) }
- 构建二进制,进行执行
$ go build command-line-arguments.go $ ./command-line-arguments a b c d [./command-line-arguments a b c d] [a b c d] c
命令行标志flag
- 命令行标志 是指定命令行程序选项的常用方法。例如,在 中wc -l是-l一个命令行标志。
- 命令行标志有多种类型,string,int,bool
- flag.String函数返回一个字符串指针(而不是字符串值)
flag.String("word", "foo", "a string")
声明标志名称,默认值和简短描述的字符串标flag.Parse()
执行命令行解析。不解析收不到参数,所有的参数值都会用默认值
flag.StringVar(&svar, "svar", "bar", "a string var")
- 未被指定标志名称的,统一由
flag.Args()
接收 声明一个使用在程序其他地方声明的现有变量的选项可以理解为将指定命令行参数的标志赋值给已经声明的变量
- 传递一个未声明的命令行参数会报错
// 基本标志声明可用于字符串、整数和布尔选项。这里我们声明一个word具有默认值"foo" 和简短描述的字符串标志。此flag.String函数返回一个字符串指针(而不是字符串值);我们将在下面看到如何使用此指针。 wordPtr := flag.String("word", "foo", "a string") // 声明numb,fork,使用与标志类似的方法word。 numbPtr := flag.Int("numb", 42, "an int") forkPtr := flag.Bool("fork", false, "a bool") // 声明一个使用在程序其他地方声明的现有变量的选项。请注意,我们需要将一个指针传递给标志声明函数 var svar string flag.StringVar(&svar, "svar", "bar", "a string var") // 一旦声明了所有标志,就调用flag.Parse() 来执行命令行解析。 flag.Parse() //这里我们只转储解析的选项和任何尾随位置参数。请注意,我们需要使用 取消引用指针*wordPtr 以获取实际的选项值。 fmt.Println("word:", *wordPtr) fmt.Println("numb:", *numbPtr) fmt.Println("fork:", *forkPtr) fmt.Println("svar:", svar) fmt.Println("tail:", flag.Args())
- 执行&验证
# 要试验命令行标志程序,最好先编译它,然后直接运行生成的二进制文件。 $ go build command-line-flags.go
# 首先赋予所有标志的值,以试用所构建的程序。 $ ./command-line-flags -word=opt -numb=7 -fork -svar=flag word: opt numb: 7 fork: true svar: flag tail: []
# 请注意,如果省略标志,它们将自动采用其默认值。 $ ./command-line-flags -word=opt word: opt numb: 42 fork: false svar: bar tail: []
# 可以在任何标志之后提供尾随位置参数。 $ ./command-line-flags -word=opt a1 a2 a3 word: opt ... tail: [a1 a2 a3]
# 请注意,该flag包要求所有标志都出现在位置参数之前(否则标志将被解释为位置参数)。 $ ./command-line-flags -word=opt a1 a2 a3 -numb=7 word: opt numb: 42 fork: false svar: bar tail: [a1 a2 a3 -numb=7]
# 使用-h或--help标志来获取命令行程序的自动生成的帮助文本。 $ ./command-line-flags -h Usage of ./command-line-flags: -fork=false: a bool -numb=42: an int -svar="bar": a string var -word="foo": a string
# 如果您提供未在包中指定的标志 flag,程序将打印错误消息并再次显示帮助文本。 # 传递一个未声明的命令行参数会报错 $ ./command-line-flags -wat flag provided but not defined: -wat Usage of ./command-line-flags: ...
命令行子命令
- 一些命令行工具(如go工具 或 )git 有许多子命令,每个子命令都有自己的一组标志。例如,go build和go get是工具的两个不同子命令go。该flag包让我们可以轻松定义具有自己标志的简单子命令。
- 声明新的字命令
flag.NewFlagSet("foo", flag.ExitOnError)
- 使子命令参数生效
fooCmd.Parse(os.Args[2:])
,也需要解析参数,和flag.Parse
解析的不一样,子命令解析需要指定切片数据位置 - 直接跟子命令标识,不需要-符号指定
func main() { // 使用该NewFlagSet 函数声明一个子命令,并继续定义特定于该子命令的新标志 fooCmd := flag.NewFlagSet("foo", flag.ExitOnError) fooEnable := fooCmd.Bool("enable", false, "enable") fooName := fooCmd.String("name", "", "name") // 对于不同的子命令,我们可以定义不同的支持标志。 barCmd := flag.NewFlagSet("bar", flag.ExitOnError) barLevel := barCmd.Int("level", 0, "level") // 子命令是该程序的第一个参数 // 判断子命令参数长度 if len(os.Args) < 2 { fmt.Println("expected 'foo' or 'bar' subcommands") os.Exit(1) } // 调用了哪个子命令。 switch os.Args[1] { // 对于每个子命令,我们解析其自己的标志并可以访问尾随的位置参数。 case "foo": fooCmd.Parse(os.Args[2:]) fmt.Println("subcommand 'foo'") fmt.Println(" enable:", *fooEnable) fmt.Println(" name:", *fooName) fmt.Println(" tail:", fooCmd.Args()) case "bar": barCmd.Parse(os.Args[2:]) fmt.Println("subcommand 'bar'") fmt.Println(" level:", *barLevel) fmt.Println(" tail:", barCmd.Args()) default: fmt.Println("expected 'foo' or 'bar' subcommand") os.Exit(1) } }
- 执行&调用
# 先编译执行程序 $ go build command-line-subcommands.go
# 首先调用 foo 子命令。 $ ./command-line-subcommands foo -enable -name=joe a1 a2 subcommand 'foo' enable: true name: joe tail: [a1 a2]
$ ./command-line-subcommands bar -level 8 a1 subcommand 'bar' level: 8 tail: [a1]
# 但是 bar 不会接受 foo 的标志(不会接受foo的命令参数)。 $ ./command-line-subcommands bar -enable a1 flag provided but not defined: -enable Usage of bar: -level int level
环境变量os.Setenv
,os.Getenv
,os.Environ()
- 环境变量是向 Unix 程序传递配置信息的 通用机制
- 设置键值对,类型为字符串
os.Setenv("FOO", "1")
,os.Getenv("FOO")
- 获取一个不存的环境变量,返回空字符串
- 获取读取到的全部变量
os.Environ()
,返回环境变量切片,需要切割strings.SplitN()
,分出变量名和值func main() { // 使用os.Setenv。要获取键的值,请使用os.Getenv。如果环境中不存在该键,这将返回一个空字符串。 os.Setenv("FOO", "1") fmt.Println("FOO:", os.Getenv("FOO")) fmt.Println("BAR:", os.Getenv("BAR")) fmt.Println() // os.Environ列出环境中的所有键/值对。这将返回格式为 的字符串片段KEY=value。您可以使用strings.SplitN它们来获取键和值。这里我们打印所有键。 for _, e := range os.Environ() { pair := strings.SplitN(e, "=", 2) fmt.Println(pair[0]) } }
- 边设置变量,边取环境变量
# BAR首先设置环境,则正在运行的程序将选择该值。 $ BAR=2 go run environment-variables.go FOO: 1 BAR: 2
日志记录log
,log\slog
- Go 标准库提供了用于从 Go 程序输出日志的简单工具,其中log包用于自由格式输出, log/slog包用于结构化输出。
- 通过日志包默认打印出来的记录带时间
- 可以通过设置
log.LstdFlags | log.Lmicroseconds
处理日志的前置格式 - 示例一个新的log,指定格式和前缀
log.New(os.Stdout, "my:", log.LstdFlags)
- 自定义输出目标,通过设置缓冲变量
var buf bytes.Buffer
,实例化日志,日志数据由变量接收,打印缓冲值 - 自定义输出,通过
buf.String()
, 调用buffer的 String() 方法获取内容 - 实例化日志时区分内容输出到标准输出
stdout``mylog := log.New(os.Stdout, "my:", log.LstdFlags)
,还是缓冲区输出buflog := log.New(&buf, "buf:", log.LstdFlags)
- 日志格式化json的方式,借助
slog
包,slog.NewJSONHandler(os.Stderr, nil)
,实例化一个操作器,把操作器传给新实例func main() { // 只需Println从 log包中调用诸如这样的函数即可使用标准记录器,该记录器已预先配置为合理的日志输出到。诸如或之os.Stderr类的其他方法 将在记录后退出程序。 log.Println("standard logger") // 2024/09/06 19:32:18 standard logger // 可以使用标志配置记录器以设置其输出格式。默认情况下,标准记录器设置了log.Ldate和log.Ltime标志,这些标志收集在中log.LstdFlags。例如,我们可以更改其标志以发出具有微秒精度的时间。 // 设置了毫秒标识,日志里面的时间就会时毫秒级别的 log.SetFlags(log.LstdFlags | log.Lmicroseconds) log.Println("with micro") // 2024/09/06 19:32:18.457625 witch micro // 它还支持发出调用该函数的文件名和行log。 // 设置了文件名函数,打印的日志会记录文件和函数,类似php中的 $e->getFile(),getLine() log.SetFlags(log.LstdFlags | log.Lshortfile) log.Println("with file/line")// 2024/09/06 19:32:18 logging.go:18: with file/line // 创建自定义记录器并传递它可能会很有用。创建新记录器时,我们可以设置前缀以将其输出与其他记录器区分开来。 // 实例化一个新的日志,设置前缀,标志【flag】相关的 // 设置了前缀 my:, 打印的日志里面会携带前缀 mylog := log.New(os.Stdout, "my:", log.LstdFlags) mylog.Println("from mylog") // my:2024/09/06 19:32:18 from mylog // 我们可以使用该方法在现有的记录器(包括标准记录器)上设置前缀SetPrefix。 // 随时修改自定义记录器的前缀 mylog.SetPrefix("ohmy:") // 携带新的前缀 mylog.Println("from mylog") // ohmy:2024/09/06 19:32:18 from mylog // 记录器可以有自定义的输出目标;任何io.Writer作品。 var buf bytes.Buffer // 此处使用的是指针,最终需要调用 变量的String() 方法 buflog := log.New(&buf, "buf:", log.LstdFlags) // 此调用将日志输出写入buf。不会进行输出 buflog.Println("hello") // 这实际上会将其显示在标准输出上。 fmt.Print("from buflog:", buf.String()) // from buflog: buf:2024/09/06 19:32:18 hello // 该slog包提供 结构化的日志输出。例如,以 JSON 格式记录日志非常简单。 jsonHandler := slog.NewJSONHandler(os.Stderr, nil) myslog := slog.New(jsonHandler) myslog.Info("hi there") // {"time":"2024-09-06T19:32:18.457669+08:00","level":"INFO","msg":"hi there"} // 除了消息之外,slog输出还可以包含任意数量的键=值对。 myslog.Info("hello again", "key", "val", "age", 25) // {"time":"2024-09-06T19:32:18.457699+08:00","level":"INFO","msg":"hello again","key":"val","age":25} }
- 为了在网站上清晰呈现,这些内容被包装起来;实际上,它们是在一行上发出的。(待确认)
HTTP客户端net/http
- 直接调用
resp, err := http.Get("https://gobyexample.com")
- 获取响应状态
esp.Status
- 对respons 实列化一个扫描对象
scanner := bufio.NewScanner(resp.Body)
- 开始扫描 通过for 配合
scanner.Scan()
func main() { // 向服务器发出 HTTP GET 请求。是创建 对象并调用其方法的http.Get便捷方式;它使用 具有有用的默认设置的对象。http.ClientGethttp.DefaultClient resp, err := http.Get("https://gobyexample.com") if err != nil { panic(err) } defer resp.Body.Close() // 打印HTTP响应状态。 fmt.Println("Response status:", resp.Status) // 对respons 实列化一个扫描对象 scanner := bufio.NewScanner(resp.Body) // 打印响应主体的前5行。 // for 配合 scanner.Scan() ,打印扫描结果 for i := 0; scanner.Scan() && i < 5; i++ { fmt.Println(scanner.Text()) } if err = scanner.Err();err != nil { panic(err) } }
http 服务器
- 获取请求实例和响应体
w http.ResponseWriter, req *http.Request
- 简单响应
fmt.Fprintf(w,"hello\n")
// net/http服务器 中的一个基本概念是处理程序。处理程序是实现接口的对象 http.Handler。编写处理程序的常用方法是使用http.HandlerFunc具有适当签名的函数上的适配器。 func hello(w http.ResponseWriter, req *http.Request) { // 充当处理程序的函数以 http.ResponseWriter和 ahttp.Request作为参数。响应编写器用于填写 HTTP 响应。这里我们的简单响应只是“hello\n”。 fmt.Fprintf(w, "hello\n") } func headers(w http.ResponseWriter, req *http.Request) { // 该处理程序通过读取所有 HTTP 请求标头并将它们回显到响应主体中,执行一些更复杂的事情。 for name, headers := range req.Header { for _, h := range headers { fmt.Fprintf(w, "%v: %v\n", name, h) } } } func main() { // 我们使用便捷函数在服务器路由上注册处理程序 。它在包中http.HandleFunc设置默认路由器net/http并将函数作为参数。 http.HandleFunc("/hello", hello) http.HandleFunc("/headers", headers) // 最后,我们ListenAndServe使用端口和处理程序调用。nil告诉它使用我们刚刚设置的默认路由器。 http.ListenAndServe(":8090", nil) }
- 后台运行
$ go run http-servers.go & $ curl localhost:8090/hello hello
上下文
- HTTP 服务器对于演示控制取消的用法很有用。AContext跨 API 边界和 goroutine 携带截止时间、取消信号和其他请求范围的值。
- context.Context机器会针对每个请求创建一个net/http,并且可以通过该Context()方法使用。
func hello(w http.ResponseWriter, req *http.Request) { // context.Context机器会针对每个请求创建一个net/http,并且可以通过该Context()方法使用。 ctx := req.Context() fmt.Println("server: hello handler started") defer fmt.Println("server: hello handler ended") // 等待几秒钟再向客户端发送回复。这可以模拟服务器正在进行的一些工作。在工作期间,请密切关注上下文的通道, Done()以查看是否收到我们应该取消工作并尽快返回的信号。 select { case <-time.After(10 * time.Second): fmt.Fprintf(w, "hello\n") case <-ctx.Done(): // 上下文的Err()方法返回一个错误,解释为什么Done()通道被关闭。 err := ctx.Err() fmt.Println("server:", err) internalError := http.StatusInternalServerError http.Error(w, err.Error(), internalError) } } func main() { // 和以前一样,我们在“/hello”路线上注册我们的处理程序,并开始服务。 http.HandleFunc("/hello", hello) http.ListenAndServe(":8090", nil) }
- 在后台运行服务器。
$ go run context.go & # 模拟客户端请求/hello,在开始后不久按 Ctrl+C 来发出取消信号。 $ curl localhost:8090/hello server: hello handler started ^C server: context canceled server: hello handler ended
生成进程os/exec
包,exec.Command()
- 有时 Go 程序需要生成其他非 Go 进程。
- exec.Command会创建一个对象来表示这个外部进程。
- io 读取输出相关操纵
io.ReadAll()
- 返回错误-获取错误类型
*exec.Error
,*exec.ExitError
, 类型断言e.(type)
func main() { //将从一个简单的命令开始,该命令不接受任何参数或输入,只会将一些内容打印到标准输出。助手exec.Command会创建一个对象来表示这个外部进程。 dateCmd := exec.Command("date") // 该Output方法运行命令,等待其完成并收集其标准输出。如果没有错误,dateOut将保存包含日期信息的字节。 dateOut, err := dateCmd.Output() if err != nil { panic(err) } fmt.Println("> date") fmt.Println(string(dateOut)) // Output如果执行命令时出现问题(例如路径错误),或者 命令运行但以非零返回代码退出,则其他方法Command将返回 。*exec.Error*exec.ExitError _, err = exec.Command("date", "-x").Output() if err != nil { // 类型断言 switch e := err.(type) { case *exec.Error: fmt.Println("failed executing:", err) case *exec.ExitError: fmt.Println("command exit rc =", e.ExitCode()) default: panic(err) } } // 接下来,我们将研究一个稍微复杂的案例,我们将数据通过管道传输到其外部进程 stdin并从其收集结果stdout。 grepCmd := exec.Command("grep", "hello") // 在这里,我们明确地抓住输入/输出管道,启动进程,向其中写入一些输入,读取结果输出,最后等待进程退出。 grepIn, _ := grepCmd.StdinPipe() grepOut, _ := grepCmd.StdoutPipe() grepCmd.Start() grepIn.Write([]byte("hello grep\ngoodbye grep")) grepIn.Close() grepBytes, _ := io.ReadAll(grepOut) grepCmd.Wait() // 我们在上面的例子中省略了错误检查,但您可以if err != nil对所有错误检查使用通常的模式。我们也只收集结果StdoutPipe ,但您可以StderrPipe以完全相同的方式收集。 fmt.Println("> grep hello") fmt.Println(string(grepBytes)) // 请注意,生成命令时,我们需要提供明确划定的命令和参数数组,而不是只传入一个命令行字符串。如果要生成带有字符串的完整命令,可以使用 的bash选项-c : lsCmd := exec.Command("bash", "-c", "ls -a -l -h") lsOut, err := lsCmd.Output() if err != nil { panic(err) } fmt.Println("> ls -a -l -h") fmt.Println(string(lsOut)) }
- 生成的程序返回的输出与我们直接从命令行运行它们的输出相同。
$ go run spawning-processes.go > date Thu 05 May 2022 10:10:12 PM PDT # 日期没有-x标志,因此它将退出并显示错误消息和非零返回代码。 command exited with rc = 1 > grep hello hello grep > ls -a -l -h drwxr-xr-x 4 mark 136B Oct 3 16:29 . drwxr-xr-x 91 mark 3.0K Oct 3 12:50 .. -rw-r--r-- 1 mark 1.3K Oct 3 16:28 spawning-processes.go
执行流程
- 当我们需要一个可供正在运行的 Go 进程访问的外部进程时,我们会这样做。有时我们只是想用另一个(可能是非 Go)进程完全替换当前的 Go 进程。为此,我们将使用 Go 的经典 exec 函数实现。
- 查找命令的路径
exec.LookPath()
- Go 不提供经典的 Unixfork 函数。不过通常这不是问题,因为启动 goroutine、生成进程和执行进程涵盖了 的大多数用例fork。
- Exec需要切片形式的参数(而不是一个大字符串)。我们将给出ls一些常见参数。请注意,第一个参数应该是程序名称。
syscall.Exec
调用func main() { // 在我们的示例中,我们将执行ls。Go 需要我们想要执行的二进制文件的绝对路径,因此我们将使用exec.LookPath它来找到它(可能是 /bin/ls)。 binary, lookErr := exec.LookPath("ls") if lookErr != nil { panic(lookErr) } // Exec需要切片形式的参数(而不是一个大字符串)。我们将给出ls一些常见参数。请注意,第一个参数应该是程序名称。 args := []string{"ls", "-a", "-l", "-h"} // Exec还需要一组环境变量 来使用。这里我们只提供我们当前的环境。 env := os.Environ() // 这是实际的syscall.Exec调用。如果此调用成功,我们的流程的执行将在这里结束,并由流程替换/bin/ls -a -l -h 。如果出现错误,我们将获得返回值。 execErr := syscall.Exec(binary, args, env) if execErr != nil { panic(execErr) } }
# 当我们运行我们的程序时,它被替换为ls。 $ go run execing-processes.go total 16 drwxr-xr-x 4 mark 136B Oct 3 16:29 . drwxr-xr-x 91 mark 3.0K Oct 3 12:50 .. -rw-r--r-- 1 mark 1.3K Oct 3 16:28 execing-processes.go
信号os.Signal
- 有时我们希望 Go 程序能够智能地处理Unix 信号。例如,我们可能希望服务器在收到 时正常关闭SIGTERM,或者命令行工具在收到 时停止处理输入SIGINT。以下是如何在 Go 中使用通道处理信号。
- Go 信号通知通过os.Signal 在通道上发送值来工作
signal.Notify
注册给定的通道来接收指定信号的通知func main() { // Go 信号通知通过os.Signal 在通道上发送值来工作。我们将创建一个通道来接收这些通知。请注意,此通道应该是缓冲的。 sigs := make(chan os.Signal, 1) // signal.Notify注册给定的通道来接收指定信号的通知。 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) // 我们可以从sigs主函数中接收,但是让我们看看如何在单独的 goroutine 中完成此操作,以演示更现实的正常关闭场景。 done := make(chan bool, 1) // 这个 goroutine 执行一个阻塞信号接收。当它收到一个信号时,它会将其打印出来,然后通知程序它可以完成了。 go func() { sig := <-sigs fmt.Println() fmt.Println(sig) done <- true }() // 程序会在这里等待,直到得到预期的信号(如上面的 goroutine 发送值所示done)然后退出。 fmt.Println("awaiting signal") <-done fmt.Println("exiting") }
- 当我们运行此程序时,它将阻塞以等待信号。通过输入ctrl-C(终端显示为^C),我们可以发送SIGINT信号,导致程序打印interrupt然后退出。
$ go run signals.go awaiting signal ^C interrupt exiting
退出
-
用于os.Exit以给定状态立即退出
-
defer使用时不会运行s os.Exit,因此fmt.Println永远不会调用它。
使用defer, 中途有os.Exit(),defer 没有机会执行
func main() { // defer使用时不会运行 os.Exit,因此fmt.Println永远不会调用它。 // 直接被退出了,所以不会执行 defer fmt.Println("!") // 以状态 3 退出。 os.Exit(3) }
-
请注意,与 C 不同,Go 不使用整数返回值来main指示退出状态。如果您想以非零状态退出,则应使用os.Exit。
-
如果您exit.go使用运行go run,则退出将被拾取go并打印。
$ go run exit.go exit status 3 # 通过echo $? 是获取不到状态值为3的,只能获取到到1
-
通过构建和执行二进制文件,您可以在终端中看到状态。
编译后,exit status 3 不会打印出来, 通过 echo $? 可以获取状态值
$ go build exit.go $ ./exit $ echo $? 3 ```shell
-
请注意,!我们的程序从未被打印出来。
-
Go语言的GMP模型是其并发编程的重要组成部分,GMP代表的是 goroutines、machine(线程)和procs(处理器)。下面是对GMP模型的详细讲解:
1. Goroutines
- 定义:Goroutines是Go语言中一种轻量级的线程管理方式,它们由Go的运行时进行调度。你可以通过使用
go
关键字来创建新的goroutine。 - 特点:
- 占用较小内存:goroutine的初始栈大小通常是2KB,栈会根据需要自动增长。
- 操作简单:通过关键字
go
启动,系统会自动管理。
2. Machines(线程)
- 定义:机器指的是运行goroutines的操作系统线程(OS threads)。Go的运行时会将多个goroutine调度到这些操作系统线程上进行执行。
- 特点:
- Go运行时会根据需求动态创建和分配OS线程,以提升并发性能。
3. Procs(处理器)
- 定义:Procs表示的是逻辑处理器的数量,Go运行时可以使用多个线程来对应多个逻辑处理器,以充分利用多核 CPU 的能力。
- 控制:你可以通过调用
runtime.GOMAXPROCS(n)
来设置最大的逻辑处理器数量,n通常设置为可用的CPU核心数。
GMP模型的调度
Go的调度器使用M:N调度模型,M个goroutines可以在N个OS线程上运行,这意味着多个goroutines可以共享较少数量的OS线程。这种机制带来了以下优点:
- 高效的资源利用率:只有在需要的时候,才会使用OS线程。
- 避免线程的开销:创建和销毁操作系统线程的开销较大,而goroutine的调度与管理开销相对较小。
- 平衡负载:Go的调度器可以根据负载情况,将goroutine动态分配到可用的OS线程上。
同步机制
为了控制goroutines之间的并发访问,Go提供了多种同步机制:
- Channels:用于在goroutines之间传递数据,具有类型安全特性。
- Mutex(互斥锁):用于保护共享数据,防止数据竞争。
- WaitGroup:用于等待一组goroutines完成。
小结
GMP模型使得Go语言能够高效地处理并发任务,通过调度轻量级的goroutines而非传统的线程,大幅提高了开发效率和程序的执行性能。理解和灵活运用GMP模型是开发高效Go应用的关键所在。
如果你有更多具体问题或需要更深入的解释,请随时告诉我!
其他
time 时间
- time.Now 返回的是个结构体
- 小时获取 t.Hour()
t := time.Now() t.Hour()
获取接口值的类型
- i.(type)
whatAmI := func(i interface{}) { switch t := i.(type) { default: fmt.Printf("Don't know type %T\n", t) } }
slice删除
接口的使用
- https://jordanorelli.com/post/32665860244/how-to-use-interfaces-in-go
error
- https://pkg.go.dev/errors
- https://go.dev/blog/go1.13-errors
协程里面return ??? 销毁协程
errorgroup 包
time.Ticker 和 time.NewTicker 区别
wiatGroup 传入到函数时应该使用指针
time.Tick 针对阻塞channel能否放进去
- 有无缓冲都不影响,和正常的channel 一样,会阻塞
千峰
导入
- 通过 . 导入,直接在当前包内使用函数
import . fmt
func main(){
Println("aaa") // 不需要fmt.Println
}