【一些笔记】go 1.23 版本工程中的重点内容
速通go 1.23
go 1.23更新了几个星期了,在刚发布的时候就在自己公司内部做了分享,然后就一直在忙其他的事,现在想起来了把文档发到鱼排与鱼油们分享一下
快速阅览
- linkename被禁用,1.23后没办法使用了
但是有逃生通道
--checklinkname=0
来关闭检查
encoding/binary
- 新增Encode、Decode、Append函数
// 将多个数据编排到一起dd func Append(buf []byte, order ByteOrder, data any) ([]byte, error) // 将给定的字节以给定的字节序编码到buf中,buf太小会返回错误,不然返回写入的字节数 func Encode(buf []byte, order ByteOrder, data any) (int, error) // 将给定的二进制数据从buf解码到data中,以给定的字节序进行解码。buf太小会返回,不然返回从buf中消耗的字节数。 func Decode(buf []byte, order ByteOrder, data any) (int, error)
- 标准库新包
structs
作为一个标记类型,用来标记当前结构体是使用当前系统的内存布局,而不是使用go的内存布局
net
包新增了一个KeepAliveConfig
的结构体
可以更细粒度的设置KeepAlive
type KeepAliveConfig struct { // If Enable is true, keep-alive probes are enabled. Enable bool // Idle is the time that the connection must be idle before // the first keep-alive probe is sent. // If zero, a default value of 15 seconds is used. Idle time.Duration // Interval is the time between keep-alive probes. // If zero, a default value of 15 seconds is used. Interval time.Duration // Count is the maximum number of keep-alive probes that // can go unanswered before dropping a connection. // If zero, a default value of 9 is used. Count int }
net
包给DNSError
新增上下文超时或取消引起的错误信息
例如:
http://errors.Is
(someDNSErr, context.DeadlineExceedeed)
net/http
包Cookie
类型Request
新增方法CookiesNamed
来根据Cookie名获取指定的CookieCookie
字符串Parse后会保留值的引号,还新增了Cookie.Quete字段,用来确认Cookie.Value是否存在引号- 新增了Partitioned字段用来区分Cookie
目前不知道有啥用
- 新增了两个函数
ParseCookie
和ParseSetCookie
用来Parse Cookie
func ParseCookie(line string) ([]*Cookie, error) func ParseSetCookie(line string) (*Cookie, error)
net/netip
修复
reflect.DeepEqual
行为与其他比较函数比较结果不一致的bug
runtime/pprof
alloc、mutex、block、threadcreate和goroutine配置文件的最大堆栈深度已从32提高至128帧
runtime/trace
panic提供更完全的跟踪信息
slices
新增Repeat函数根据给定次数重复生成给定切片内容
func Repeat[S ~[]E, E any](x S, count int) S
sync
Map新增Clear函数
func (m *Map) Clear()
time
现在Parse和ParseLocation会在时区偏移超出时返回错误
os.CopyFS
递归复制一个目录到另一个目录
src := os.DirFS("/foo/") dst := "/bar/" err := os.CopyFS(dst, src) if err != nil { panic(err) } fmt.Printf("copied %s to %s\n", src, dst)
- system
- Macos需要macOS 11 Big Sur及以上的版本才能运行1.23
- 1.23是最后支持linux kernel 2.6.32的版本,1.24开始将需要linux kernel 3.17及以上的版本
计时器
1.23的计时器 time.Timer
和 time.Ticker
修复了两个重要问题,一个是释放问题一个是重置问题
释放问题
time.After
在1.23之前循环使用是一个不安全的行为,可能会造成大量的内存得不到释放
// go 1.22 type token struct{} func consumer(ctx context.Context, in <-chan token) { for { select { case <-in: // do stuff case <-time.After(time.Hour): // log warning case <-ctx.Done(): return } } } // 返回gc后的堆的字节大小 func getAlloc() uint64 { var m runtime.MemStats runtime.GC() runtime.ReadMemStats(&m) return m.Alloc } func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() tokens := make(chan token) go consumer(ctx, tokens) memBefore := getAlloc() // 循环10万次 for range 100000 { tokens <- token{} } memAfter := getAlloc() memUsed := memAfter - memBefore fmt.Printf("Memory used: %d KB\n", memUsed/1024) // Memory used: 24477 KB }
原因是 time.After
会创建一个触发了才会释放的计时器,由于使用了较大的时间(这里是一个小时) consumer
会创建大量还没有触发的计时器,因此gc没办法把它们释放掉,只能等到它们触发后才释放
到1.23后这个行为被修复了,所有的 time.Timer
和 time.Ticker
都将在没有被引用的时候释放,不会等到 触发了才释放(time.After
是 time.Timer
的封装),所以在1.23循环使用是一个安全的行为
// go 1.23 func main() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() tokens := make(chan token) go consumer(ctx, tokens) memBefore := getAlloc() // 循环10万次 for range 100000 { tokens <- token{} } memAfter := getAlloc() memUsed := memAfter - memBefore fmt.Printf("Memory used: %d KB\n", memUsed/1024) // Memory used: 11 KB }
重置问题
在1.23以前的版本中,time.Timer
和 time.Ticker
的 Reset
方法在一些场景上出现非 预期行为
// go 1.22 func main() { t := time.NewTimer(10 * time.Millisecond) time.Sleep(20 * time.Millisecond) start := time.Now() t.Reset(timeout) <-t.C fmt.Printf("Time elapsed: %dms\n", time.Since(start).Milliseconds()) // Time elapsed: 0ms }
这里新建的计时器是10毫秒,sleep
20毫秒后这个时候已经超时了,这个时候重置计时器, 预期行为应该是在 <-t.C
上阻塞10毫秒,然后打印 Time elapsed: 10ms
,但这却输出了 Time elapsed: 0ms
这是因为在1.23之前的计时器的信号通道的容量都是1
// go 1.22 package main import ( "fmt" "time" ) func main() { t := time.NewTimer(time.Second) fmt.Printf("cap(t.C): %d\n", cap(t.C)) // cap(t.C): 1 }
当计时器被触发后,就会往 t.C
这个通道发送一个信号,但 Reset
方法并不会排空通道 中的信号,当要读取 t.C
的时,通道里还存有上一次触发的信号,导致重置失败
// go 1.23 func main() { t := time.NewTimer(10 * time.Millisecond) time.Sleep(20 * time.Millisecond) start := time.Now() t.Reset(timeout) <-t.C fmt.Printf("Time elapsed: %dms\n", time.Since(start).Milliseconds()) fmt.Printf("cap(t.C): %d\n", cap(t.C)) // Time elapsed: 10ms // cap(t.C): 0 }
迭代器接口
1.23添加了 rangefunc
的迭代器接口,以及配套的标准库包 iter
, 只要方法或者函数满足以下接口就可以使用 for range
对其迭代
func(yield func()) func(yield func(T)) func(yield func(K, V))
iter
标准库中有对其定义
type Seq[V any] func(yield func(V) bool) type Seq2[K, V any] func(yield func(K, V) bool)
标准库没有对空参数的yield进行定义,wiki中是存在的,但是发行的时候没有,应该是被去除了
提案中将 Seq
类的迭代器叫 push iterators
// Example // push iterators func All[T any](xs []T) iter.Seq[T] { var ( i int boundary = len(xs) ) return func(yield func(T) bool) { for i < boundary { if !yield(xs[i]) { return } i++ } } } func main() { m := 0 for n := range All([]int{1, 2, 3}) { m += n return } }
for range
迭代All函数最终会被编译成
All([]int{1, 2, 3})(func(n int){ m += n return true })
这只是有让人一个概念,不要真的只当作成这样
这样可以直接使用 for-range
遍历 sync.Map
var m sync.Map m.Store("alice", 11) m.Store("bob", 12) m.Store("cindy", 13) for key, val := range m.Range { fmt.Println(key, val) } // alice 11 // bob 12 // cindy 13
iter
标准库中还有两个函数
func Pull[V any](Seq[V]) (next func() (V, bool), stop func()) func Pull2[K, V any](Seq2[K, V]) (next func() (K, V, bool), stop func())
这两个函数在提案中称为 pull iterators 这个函数是将push迭代器转换成类似其他语言的迭代器,next返回值和是否还存在下一个值,stop则是结束迭代器, 在使用stop后next永远都只会返回零值和false
func main() { next, stop := iter.Pull(All([]int{1, 2, 3})) next() // 1 true next() // 2 true stop() // stop next() // 0 false next() // 0 fals }
配套的新增了以下标准库方法
- slices
// 将切片转换成iter.Seq2 func All[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] // example for i, v := range slices.All([]string{"a", "b", "c"}) { fmt.Printf("%d:%v ", i, v) } // 0:a 1:b 2:c
// 将切片转成iter.Seq不迭代下标 func Values[Slice ~[]E, E any](s Slice) iter.Seq[E] // example for v := range slices.Values([]string{"a", "b", "c"}) { fmt.Printf("%v ", v) } // a b c
// 从尾部开始遍历 func Backward[Slice ~[]E, E any](s Slice) iter.Seq2[int, E] // example for i, v := range slices.Backward([]string{"a", "b", "c"}) { fmt.Printf("%d:%v ", i, v) } // 2:c 1:b 0:a
// 将iter.Seq转换成切片 func Collect[E any](seq iter.Seq[E]) []E // example fmt.Println(slices.Collect(slices.Values([]int{11, 12, 13}))) // [11 12 13]
// 将iter.Seq追加到切片中 func AppendSeq[Slice ~[]E, E any](s Slice, seq iter.Seq[E]) Slice // example s1 := []int{11, 12} s2 := []int{13, 14} s := slices.AppendSeq(s1, slices.Values(s2)) fmt.Println(s) // [11 12 13 14]
// 排序iter.Seq到切片中 func Sorted[E cmp.Ordered](seq iter.Seq[E]) []E // example s1 := []int{13, 11, 12} s2 := slices.Sorted(slices.Values(s1)) fmt.Println(s2) // [11 12 13]
// 排序iter.Seq可以通过函数指定要排序的值 func SortedFunc[E any](seq iter.Seq[E], cmp func(E, E) int) []E // example type person struct { name string age int } s1 := []person{{"cindy", 20}, {"alice", 25}, {"bob", 30}} compare := func(p1, p2 person) int { return cmp.Compare(p1.name, p2.name) } s2 := slices.SortedFunc(slices.Values(s1), compare) fmt.Println(s2) // [{alice 25} {bob 30} {cindy 20}]
SortedStabFunc 和 SortedFunc
一样,但是算法是使用的
https://en.wikipedia.org/wiki/Sorting_algorithm#Stability
// 将切片转换成分块的iter.Seq func Chunk[Slice ~[]E, E any](s Slice, n int) iter.Seq[Slice] { // example s := []int{1, 2, 3, 4, 5} for v := range slices.Chunk(s, 2) { fmt.Printf("%v ", v) } // [1 2] [3 4] [5]
- maps
// 将map转换成iter.Seq2 func All[Map ~map[K]V, K comparable, V any](m Map) iter.Seq2[K, V] // example for k, v := range maps.All(map[string]int{"a": 1, "b": 2, "c": 3}) { fmt.Printf("%v:%v ", k, v) } // a:1 b:2 c:3
// 迭代map中的key func Keys[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[K] // example for k := range maps.Keys(map[string]int{"a": 1, "b": 2, "c": 3}) { fmt.Printf("%v ", k) } // b c a
// 迭代map中的value func Values[Map ~map[K]V, K comparable, V any](m Map) iter.Seq[V] // example for v := range maps.Values(map[string]int{"a": 1, "b": 2, "c": 3}) { fmt.Printf("%v ", v) } // 1 2 3
// 将iter.Seq2 插入到map中,会覆盖已有的元素 func Insert[Map ~map[K]V, K comparable, V any](m Map, seq iter.Seq2[K, V]) // example m1 := map[string]int{"a": 1, "b": 2} m2 := map[string]int{"b": 12, "c": 3, "d": 4} maps.Insert(m1, maps.All(m2)) fmt.Println(m1) // map[a:1 b:12 c:3 d:4]
// 将iter.Seq2收集到新的map func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]V // example m1 := map[string]int{"a": 1, "b": 2, "c": 3} m2 := maps.Collect(maps.All(m1)) fmt.Println(m2) // map[a:1 b:2 c:3]
-
注意事项*
- 使用push迭代器的时候不要带入展开
怎么展开push迭代器完全由编译器决定,case太多有很多内联优化,直接当作正常循环使用即可
- push迭代器的yield返回值一定要处理
yield返回了false还继续调用yield将会panic
- 不在push迭代器的循环体中使用recover
循环的时候出现了panic,然后在循环体中recover之后是可以正常使用的,但是这里并不推荐这样做, 因为这个操作就连go团队也
https://tip.golang.org/wiki/RangefuncExperiment#what-happens-if-the-iterator-function-recovers-a-panic-in-the-loop-body
所以尽量避免使用这种未确定的操作- 不要并发使用pull迭代器的next
pull迭代器的next并不是并发安全的,并发使用会panic
- 在使用pull迭代器的时没有将内容迭代完,必须调用stop函数
使用stop可以让迭代器提前进入结束的状态,避免未定以行为
unique
标准库新增的功能,用于对一个开销大或长期使用的值进行复用的功能,也用与频繁比较的值 假设我们有一个字符串随机生成器
const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" func randomString(n int) string { b := make([]byte, n) for i := range b { b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) } func wordGen(nDistinct, wordLen int) func() string { vocab := make([]string, nDistinct) for i := range nDistinct { word := randomString(wordLen) vocab[i] = word } return func() string { word := vocab[rand.Intn(nDistinct)] return strings.Clone(word) } }
从100个字符串中生成十万个字符串
// 从100个字符串中生成十万个字符串 var words []string const ( nWords int = 1e5 nDistinct = 100 wordLen = 50 ) func main() { generate := wordGen(nDistinct, wordLen) memBefore := getAlloc() words = make([]string, nWords) for i := range nWords { words[i] = generate() } memAfter := getAlloc() memUsed := memAfter - memBefore fmt.Printf("Memory used: %.2f MB\n", float64(memUsed)/1024/1024) // Memory used: 7.63 MB }
直接使用字符串做存储大约使用了 7.63MB
换成unique来存储
var words []unique.Handle[string] const ( nWords int = 1e5 nDistinct = 100 wordLen = 50 ) func main() { generate := wordGen(nDistinct, wordLen) memBefore := getAlloc() words = make([]unique.Handle[string], nWords) for i := range nWords { words[i] = unique.Make(generate()) } memAfter := getAlloc() memUsed := memAfter - memBefore fmt.Printf("Memory used: %.2f MB\n", float64(memUsed)/1024/1024) // Memory used: 0.79 MB }
只用了 0.79MB 内存占用差了将近十倍,数量越大越明显
进行基准测试,性能提升的也是非常之快
package unique import ( "testing" "unique" ) func CreateString() string { t := make([]byte, 10e6) for i := 0; i < 10e6; i++ {j t[i] = 'a' } return string(t) } func EqToken[T string | unique.Handle[string]](t1, t2 T) bool { return t1 == t2 } func BenchmarkEqualString(b *testing.B) { Token1 := CreateString() Token2 := CreateString() TokenUnique1 := unique.Make(Token1) TokenUnique2 := unique.Make(Token2) b.ResetTimer() b.Run("normal", func(b *testing.B) { for i := 0; i < b.N; i++ { EqToken(Token1, Token2) } }) b.Run("unique", func(b *testing.B) { for i := 0; i < b.N; i++ { EqToken(TokenUnique1, TokenUnique2) } }) } // goos: linux // goarch: amd64 // pkg: test/unique // cpu: AMD Ryzen 7 5800U with Radeon Graphics // BenchmarkEqualString/normal-16 2193 565980 ns/op // BenchmarkEqualString/normal-16 2226 517478 ns/op // BenchmarkEqualString/normal-16 2008 559273 ns/op // BenchmarkEqualString/normal-16 2007 577643 ns/op // BenchmarkEqualString/normal-16 2226 561853 ns/op // BenchmarkEqualString/unique-16 1000000000 0.2629 ns/op // BenchmarkEqualString/unique-16 1000000000 0.2517 ns/op // BenchmarkEqualString/unique-16 1000000000 0.2431 ns/op // BenchmarkEqualString/unique-16 1000000000 0.2672 ns/op // BenchmarkEqualString/unique-16 1000000000 0.2421 ns/op // PASS // ok test/unique 7.646s
unique的实现原理大致为全局并发安全的HashTrieMap + weak pointer,类似与 map[any]*T
其中的 *T
是weak pointer 避免HashTrieMap无限增长,unique会在每次gc后会遍历HashTrieMap将所有指向空指针的weak pointer清理掉
weak pointer这个概念的时候,平常的普通的指针就变成了strong pointer, 当有一个strong pointer在,指向一个值的时候,这个值的引用计数就会+1, 这样只有这个引用计数在变成0的时候这个值才会被gc掉, weak pointer与strong pointer的区别就是weak pointer在指向一个值的时候, 这个值的引用计数并不会+1,好处是引用了一个值,但是这个引 用不会因为影响gc对这个值的回收。 在go 1.24中可能会加入到标准库,go 1.23还只是在go编译器的internal中
- 注意事项
- 弱指针本身也是一个值
弱指针虽然指向其他值不会增加标记或计数,但是其本身也是一个正常的值,本身被其他指针指向的时候也是会产生标记和计数 - unique.Handle一定要管理好生命周期
unique.Handle关系着全局HashTrieMap中的弱指针是否能释放,没管理好会出现内存泄漏的风险 不过说是这样说,go是有gc的语言,一般正常业务上是不会出现没办法释放的情况,在go中大概 也就只有协程没办法退出导致的泄漏了,不需要特别担心
- 弱指针本身也是一个值
atomic number
atomic的数字类型都新增了两个方法,And
和 Or
,分别对应数字类型的 &
和 |
运算符
const m = 0b100 var n atomic.Int32 n.Store(m) fmt.Printf("%b -> %b\n", n.Or(0b010), n.Load()) // 100 -> 110
const m = 0b100 var n atomic.Int32 n.Store(m) fmt.Printf("%b -> %b\n", n.And(0b010), n.Load()) // 100 -> 0
And
和 Or
两个方法在调用后都会返回之前的值
与这两个方法配套的函数是以两个方法名开头的函数,比如atomic.OrInt32
、atomic.AndInt32
,每个数字类型都有相应的函数
参考引用
https://github.com/golang/go/issues/61405
https://github.com/golang/go/issues/61897
https://tip.golang.org/wiki/RangefuncExperiment#what-if-the-iterator-function-ignores-yield-returning-false
https://go.googlesource.com/go/+/refs/changes/41/510541/7/src/cmd/compile/internal/rangefunc/rewrite.go
https://cs.opensource.google/go/go/+/master:src/unique/handle.go;bpv=0;bpt=1
https://github.com/golang/go/blob/master/src/runtime/mgc.go#LL865
https://github.com/golang/go/issues/62483
https://tip.golang.org/ref/spec#For_range
https://antonz.org/go-1-23/
大佬
水,水呢。好干
还是bula的文档详尽啊
好
666
大佬
佬登
布院士太强啦

下次多加点水,太干了
布院士太强啦

下次多加点水,太干了
这这这,阅。强
果然都是大佬,就我是个菜鸡