浅谈go1.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 一样,但是算法是使用的 稳定排序算法

// 将切片转换成分块的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团队也不确定是否是正确的操作 所以尽量避免使用这种未确定的操作

    • 不要并发使用pull迭代器的next

    pull迭代器的next并不是并发安全的,并发使用会panic

    • 在使用pull迭代器的时没有将内容迭代完,必须调用stop函数

    使用stop可以让迭代器提前进入结束的状态,避免未定以行为

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值