> ## Content Index
> Fetch the complete content index at: https://huizhou92.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# Go 1.24: 新的官方库 synctest
- URL: https://huizhou92.com/go-1-24-xin-de-guan-fang-ku-synctest/
- Published: 2024-12-19T11:22:52.000Z
- Updated: 2026-09-06T11:39:46.000Z
- Description: Go 1.24: 新的官方库 synctest. 在Go语言中，测试并发代码一直是一个具有挑战性的任务。尤其是测试一些需要跟时间相关的case的时候，传统的测试方法通常依赖于真实的系统时钟和同步机制，这会导致测试变得缓慢且容易出现不确定性。 假设我们需要测试 go-cache 的过期删除功能，我们可能会写出这。
- Author: huizhou92
- Tags: tech, golang, go1_24, #lang-zh-cn

在Go语言中，测试并发代码一直是一个具有挑战性的任务。尤其是测试一些需要跟时间相关的case的时候，传统的测试方法通常依赖于真实的系统时钟和同步机制，这会导致测试变得缓慢且容易出现不确定性。

假设我们需要测试`go-cache` 的过期删除功能，我们可能会写出这种代码

> [go-cache](https://github.com/patrickmn/go-cache?ref=huizhou92.com)是一个非常流行的 Go 缓存库，支持过期时间（TTL）和定期清理过期的缓存项。它适用于缓存数据，能够自动删除过期的条目。

list1: TestGoCacheEntryExpires

```go
func TestGoCacheEntryExpires(t *testing.T) {  
    c := cache.New(5*time.Second, 10*time.Second)  
    c.Set("foo", "bar", cache.DefaultExpiration)  
    v, found := c.Get("foo")  
    assert.True(t, found)  
    assert.Equal(t, "bar", v)  
    time.Sleep(5 * time.Second)  
    v, found = c.Get("foo")  
    assert.False(t, found)  
    assert.Nil(t, v)  
}

```

运行这个case 需要等待5s，让 `cache`过期。

```shell
➜  synctest git:(main) ✗ time go test go-cacha_test.go
ok      command-line-arguments  5.012s
go test go-cacha_test.go  0.38s user 1.27s system 27% cpu 6.049 total

```

在 `Golang 1.24` 之后，如何使用`testing/synctest` 解决这个问题呢？首先看看基于 `testing/synctest` 如何写这个test case 把。  
List 2: `TestGoCacheEntryExpiresWithSynctest`

```go
func TestGoCacheEntryExpiresWithSynctest(t *testing.T) {  
    c := cache.New(2*time.Second, 5*time.Second)  
    synctest.Run(func() {  
       c.Set("foo", "bar", cache.DefaultExpiration)  
       // Get an entry from the cache.  
       if got, exist := c.Get("foo"); !exist && got != "bar" {  
          t.Errorf("c.Get(k) = %v, want %v", got, "bar")  
       }  
  
       // Verify that we get the same entry when accessing it before the expiry.  
       time.Sleep(1 * time.Second)  
       if got, exist := c.Get("foo"); !exist && got != "bar" {  
          t.Errorf("c.Get(k) = %v, want %v", got,"bar")  
       }  
       // Wait for the entry to expire and verify that we now get a new one.  
       time.Sleep(3 * time.Second)  
       if got, exist := c.Get("foo"); exist {  
          t.Errorf("c.Get(k) = %v, want %v", got, nil)  
       }  
    })  
}

```

运行时间：

```shell
➜  synctest git:(main) ✗ time GOEXPERIMENT=synctest gotip  test -run TestGoCacheEntryExpiresWithSynctest
PASS
ok      blog-example/go/go1.24/synctest 0.009s
GOEXPERIMENT=synctest gotip test -run TestGoCacheEntryExpiresWithSynctest  0.36s user 1.23s system 171% cpu 0.933 total

```

仅仅耗时 `0.009s`

### testing/synctest 揭秘

[testing/synctest](https://github.com/golang/go/issues/67434?ref=huizhou92.com)主要目的是为了简化并发代码的测试。它的核心思想是通过使用虚拟时钟和`goroutine`组(也称为`bubble`)来控制并发代码的执行，从而使测试既快速又可靠。它仅仅只有两个接口。

```go
func Run(f func()) { synctest.Run(f) } 
func Wait() { synctest.Wait() }

```

`Run`函数在一个新的`goroutine`中执行f函数，并创建一个独立的`bubble`，确保所有相关的`goroutine`都在虚拟时钟的控制下执行。

`Wait` 用来同步 `bubble` 中`goroutine‘s` 的状态。调用该函数调用后，主`goroutine`将阻塞，直到`bubble`中的其他`goroutine`都处于`durably blocked` 状态。  
`durably blocked` 是 `bubble` 中`goroutine` 的一个特有的状态，有几种方式可以使 `goroutine` 进入 `durably blocked` 状态。

- 在`bubble`内向`channel`发送或接收数据
- select语句中，每个case都是`bubble`内的`channel`
- A send or receive on a channel from within the bubble
- A select statement where every case is a channel within the bubble
- `sync.Cond.Wait`
- `time.Sleep`  
但是如果`goroutine` 是因为 `系统调用`或`外部事件`而阻塞， 它就不是 `durably blocked` 状态，`synctest.Wait` 也不会因为它而阻塞。

每个`bubble`都有一个虚拟时钟，从`2000-01-01 00:00:00 UTC` 开始. 这个虚拟时钟不会根据代码的执行时间前进，只有在所有`goroutine`都处于空闲状态时才会向前推进。在`TestGoCacheEntryExpiresWithSynctest` 中调用`time.Sleep` 的时候， `goroutine` 只有一个并且处于空闲状态，时间会快速前进，而不必等待真实时间过去，所以测试运行速度很快。

```go
func TestSynctest(t *testing.T) {  
    synctest.Run(func() {  
       before := time.Now()  
       fmt.Println("before", before)  
       f1:=func() {  
          count := 0  
          for i := 0; i < 10e9; i++ { // time consuming, It's about 3s in my machine  
             count++  
          }  
       }
       go f1()
       synctest.Wait()  //wait f1
       after := time.Now()  // time is not affected by the running time of f1
       fmt.Println("after", after)  
    })  
}

```

运行结果为：

```shell
➜  synctest git:(main) ✗ time GOEXPERIMENT=synctest gotip  test -run TestSynctest 
before 2000-01-01 08:00:00 +0800 CST m=+946164282.733407335
after 2000-01-01 08:00:00 +0800 CST m=+946164282.733407335
PASS
ok      blog-example/go/go1.24/synctest 3.131s
GOEXPERIMENT=synctest gotip test -run TestSynctest  3.45s user 1.25s system 133% cpu 3.533 total

```

在 gosrc 中已经已经有蛮多使用 `synctest` 优化case 的例子。

![Pasted image 20241223173803](https://images.hxzhouh.com/blog-images/2024/12/68721819fa0f10742221c02f74ca3fa2.png)

参考资料：  
[https://github.com/golang/go/blob/05d8984781f7cf2f0f39b53699a558b6a1965c6c/src/testing/synctest/synctest.go#L41](https://github.com/golang/go/blob/05d8984781f7cf2f0f39b53699a558b6a1965c6c/src/testing/synctest/synctest.go?ref=huizhou92.com#L41)