goroutine中time.Now()的意外行为

huangapple go评论92阅读模式
英文:

Unexpected behaviour of time.Now() in goroutine

问题

作为熟悉Go语言的一种方式,我正在尝试构建一个(完全不可靠的)随机数生成器。我的想法是计时100个GET请求到某个URL,对结果进行处理,并生成一个“随机”数。我想知道在使用goroutine和工作组进行请求时,代码是否会运行得更快。答案似乎是肯定的,但是当打印出单个请求的计时结果时,goroutine调用的计时结果展现出了一个有趣的结果。

GET请求的顺序计时(单位:微秒):
[25007 30502 25594 40417 31505 18502 20503 19034 19473 18001 36507 25004 28005 19004 20502 20503 20503 20504 20002 19003 20511 18494 20003 21004 20003 20502 20504 19002 19004 21506 29501 30005 31005 21504 20054 22452 19503 19503 20003 19503 21004 18501 18003 20003 20003 19003 19503 20003 23504 18003 20003 19503 19502 19003 20003 20003 20040 21010 18959 20503 34251 27260 30504 25004 22004 20502 20003 19503 20502 20504 19503 22003 19003 19003 20003 20002 18003 19503 19003 18503 20504 18552 18953 18002 20003 19004 21002 18503 20503 19503 20504 20003 20003 21003 46050 19504 18503 19503 19503 19002]

使用goroutine的GET请求的计时(单位:微秒):
[104518 134570 157528 187533 193535 193535 208036 211041 220039 220242 252044 252044 258045 258045 258045 258045 271047 282050 282050 282050 286050 287050 289051 296052 297552 300052 300678 305553 307053 308054 310556 311069 312055 312555 324056 329558 334559 339559 346061 353562 360563 369564 375065 377566 384067 393569 397069 402570 410072 416572 420573 425574 431076 437576 443078 446577 453579 458580 465081 474583 480584 488085 496122 505588 510589 515590 520591 526592 533593 538596 544595 549596 555097 563098 569600 575100 584101 589604 595604 604106 610606 620609 634111 640611 645613 653119 656616 663116 669117 674118 681119 696122 709123 723627 735629 747631 757632 769635 779137 785139]

goroutine调用的计时结果是递增的,而顺序计时的结果是预期的。我怀疑这可能与time.Now()只被评估一次用于所有goroutine有关,但是调整该调用的位置并没有改变结果。

这是我目前的代码,我知道熵不是衡量随机性的好方法,但是由于某些原因我还是包含了它:

package main

import (
	"fmt"
	"log"
	"math/rand"
	"net/http"
	"sync"
	"time"

	"github.com/montanaflynn/stats"
)

func doGet(address string, channel chan int, wg *sync.WaitGroup) {
	// 在工作组中进行GET请求
	// 延迟wg信号
	defer wg.Done()
	// 开始计时
	startTime := time.Now()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	// 获取从开始到现在的时间
	delta := int(time.Since(startTime).Microseconds())
	channel <- delta
}

func doGetNoWg(address string) int {
	// 不使用工作组/通道进行GET请求
	start := time.Now()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	return int(time.Since(start).Microseconds())
}

func main() {
	var wg sync.WaitGroup
	// 初始化计时数组
	var timings_parallel [100]int
	var timings_sequential [100]int
	// 获取一个均匀的小集合用于熵的比较
	zeroes := []int{1, 1, 1}
	// 获取一个随机集合用于熵的比较
	var randnrs [100]int
	for i := 0; i < len(randnrs); i++ {
		randnrs[i] = rand.Intn(250)
	}
	// 开始
	start := time.Now()
	ch := make(chan int, 100)
	url := "https://www.nu.nl"
	wg.Add(100)
	for i, _ := range timings_parallel {
		// 可以在没有虚拟赋值或显式计数器的情况下完成吗?
		i = i
		go doGet(url, ch, &wg)
	}
	wg.Wait()
	close(ch)
	// 将通道中的结果放入结果数组
	count := 0
	for ret := range ch {
		timings_parallel[count] = ret
		count++
	}
	// 获取此部分的总运行时间
	time_parallel := time.Since(start).Milliseconds()

	// 顺序部分的开始
	start = time.Now()
	for i, _ := range timings_sequential {
		timings_sequential[i] = doGetNoWg(url)
	}
	// 顺序部分的结束。我为什么要使用goroutine呢?:P
	time_sequential := time.Since(start).Milliseconds()

	// 计算熵
	entropy, _ := stats.Entropy(stats.LoadRawData(timings_parallel[:]))
	entropy_equal, _ := stats.Entropy(stats.LoadRawData(zeroes[:]))
	entropy_random, _ := stats.Entropy(stats.LoadRawData(randnrs[:]))

	// 打印输出
	fmt.Print("Parallel: ")
	fmt.Printf("%v\n", timings_parallel)
	fmt.Print("Sequential: ")
	fmt.Printf("%v\n", timings_sequential)
	fmt.Printf("Entropy equal: %v\n", entropy_equal)
	fmt.Printf("Entropy random: %v\n", entropy_random)
	fmt.Printf("Entropy: %v\n", entropy)
	fmt.Printf("Time elapsed parallel: %v\n", time_parallel)
	fmt.Printf("Time elapsed sequential: %v", time_sequential)
}

示例输出(不包含计时数组):

熵相等:1.0986122886681096
熵随机:4.39737296171013
熵:4.527705829831552
并行耗时:786
顺序耗时:2160

所以,goroutine部分似乎要快得多,而单个计时似乎要高得多。有人知道如何正确获取计时(或者为什么计时结果如预期那样)吗?

===== 更新
goroutine的最后一个计时几乎总是等于或小于“并行耗时”中测量的总时间。

===== 更新2
问题似乎是time.Now()的第一次调用总是返回相同的时间,而第二次调用正常工作。至少这解释了结果:

GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
GOstop: 2022-04-05 18:47:06.4736105 +0200 CEST m=+0.165865901
GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
GOstop: 2022-04-05 18:47:06.4736105 +0200 CEST m=+0.165865901
...
GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
GOstop: 2022-04-05 18:47:06.6234215 +0200 CEST m=+0.315676901

英文:

As a way of trying to familiarize myself with Go I am trying to build a (completely unreliable) random number generator. The idea is to time 100 GET requests to some url, do something with the results and produce a "random" number.
I was interested in seeing if the code would run faster when using goroutines in a workgroup to do the requests. The answer seems to be yes, but when printing out the results for the timing of individual requests, the timings for the goroutine calls exhibit an interesting result.
Sequential timings of the GET requests in microseconds:
[25007 30502 25594 40417 31505 18502 20503 19034 19473 18001 36507 25004 28005 19004 20502 20503 20503 20504 20002 19003 20511 18494 20003 21004 20003 20502 20504 19002 19004 21506 29501 30005 31005 21504 20054 22452 19503 19503 20003 19503 21004 18501 18003 20003 20003 19003 19503 20003 23504 18003 20003 19503 19502 19003 20003 20003 20040 21010 18959 20503 34251 27260 30504 25004 22004 20502 20003 19503 20502 20504 19503 22003 19003 19003 20003 20002 18003 19503 19003 18503 20504 18552 18953 18002 20003 19004 21002 18503 20503 19503 20504 20003 20003 21003 46050 19504 18503 19503 19503 19002]

Goroutine timings of the GET requests in microseconds:
[104518 134570 157528 187533 193535 193535 208036 211041 220039 220242 252044 252044 258045 258045 258045 258045 271047 282050 282050 282050 286050 287050 289051 296052 297552 300052 300678 305553 307053 308054 310556 311069 312055 312555 324056 329558 334559 339559 346061 353562 360563 369564 375065 377566 384067 393569 397069 402570 410072 416572 420573 425574 431076 437576 443078 446577 453579 458580 465081 474583 480584 488085 496122 505588 510589 515590 520591 526592 533593 538596 544595 549596 555097 563098 569600 575100 584101 589604 595604 604106 610606 620609 634111 640611 645613 653119 656616 663116 669117 674118 681119 696122 709123 723627 735629 747631 757632 769635 779137 785139]
The timings for the goroutine calls are incremental, while the regular sequential timings are expected. I suspected this might have something to do with time.now() being evaluated once for all gorotines, but shuffling that call around did not change the results.
This is what I have so far, and I know entropy is not a good measure of randomness but I included it anyway because of reasons goroutine中time.Now()的意外行为
First the goroutines are run, next the sequential version is run. Lastly, timings and some other stuff gets printed.

package main

import (
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;math/rand&quot;
	&quot;net/http&quot;
	&quot;sync&quot;
	&quot;time&quot;

	&quot;github.com/montanaflynn/stats&quot;
)

func doGet(address string, channel chan int, wg *sync.WaitGroup) {
	// do get request in a workgroup
    // defer wg signal
	defer wg.Done()
	// start time
	startTime := time.Now()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	// get time since start
	delta := int(time.Since(startTime).Microseconds())
	channel &lt;- delta
}

func doGetNoWg(address string) int {
	// do get request without a workgroup/channel
	start := time.Now()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	return int(time.Since(start).Microseconds())
}

func main() {
	var wg sync.WaitGroup
	// initialize arrays for the timings
	var timings_parallel [100]int
	var timings_sequential [100]int
	// get a small uniform set for comparison of entropy
	zeroes := []int{1, 1, 1}
	// get a random set for comparison of entropy
	var randnrs [100]int
	for i := 0; i &lt; len(randnrs); i++ {
		randnrs[i] = rand.Intn(250)
	}
	// start
	start := time.Now()
	ch := make(chan int, 100)
	url := &quot;https://www.nu.nl&quot;
	wg.Add(100)
	for i, _ := range timings_parallel {
		// can this be done without dummy assignemnt or explicit counters?
		i = i
		go doGet(url, ch, &amp;wg)
	}
	wg.Wait()
	close(ch)
	// feed the results from the channel into the result array
	count := 0
	for ret := range ch {
		timings_parallel[count] = ret
		count++
	}
	// get total running time for this part
	time_parallel := time.Since(start).Milliseconds()

	// start of the sequential part
	start = time.Now()
	for i, _ := range timings_sequential {
		timings_sequential[i] = doGetNoWg(url)
	}
	// end sequential part. Why was I using goroutines again? :P
	time_sequential := time.Since(start).Milliseconds()
    
    // calculate entropy
    entropy, _ := stats.Entropy(stats.LoadRawData(timings_parallel[:]))
	entropy_equal, _ := stats.Entropy(stats.LoadRawData(zeroes[:]))
	entropy_random, _ := stats.Entropy(stats.LoadRawData(randnrs[:]))

	// print out stuffs
	fmt.Print(&quot;Parallel: &quot;)
	fmt.Printf(&quot;%v\n&quot;, timings_parallel)
	fmt.Print(&quot;Sequential: &quot;)
	fmt.Printf(&quot;%v\n&quot;, timings_sequential)
	fmt.Printf(&quot;Entropy equal: %v\n&quot;, entropy_equal)
	fmt.Printf(&quot;Entropy random: %v\n&quot;, entropy_random)
	fmt.Printf(&quot;Entropy: %v\n&quot;, entropy)
	fmt.Printf(&quot;Time elapsed parallel: %v\n&quot;, time_parallel)
	fmt.Printf(&quot;Time elapsed sequential: %v&quot;, time_sequential)

}

Example output (sans the the timing arrays):
> Entropy equal: 1.0986122886681096
> Entropy random: 4.39737296171013
> Entropy: 4.527705829831552
> Time elapsed parallel: 786
> Time elapsed sequential: 2160

So the goroutines part seems a lot faster while the individual timings seem much higher. Does anybody have an idea on how to get the timings right (or why they are expected as they are)?

===== Update
The last timing of the goroutines is pretty much always equal to or a millisecond below the total time measured in Time elapsed parallel

===== Update2
The problem seems to be that the first call to time.Now() always yields the same time, while the second time.Now() works fine. At least this explains the results:

>GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
>GOstop: 2022-04-05 18:47:06.4736105 +0200 CEST m=+0.165865901
>GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
>GOstop: 2022-04-05 18:47:06.4736105 +0200 CEST m=+0.165865901
...
>GOstart: 2022-04-05 18:47:06.3117452 +0200 CEST m=+0.004000601
>GOstop: 2022-04-05 18:47:06.6234215 +0200 CEST m=+0.315676901

答案1

得分: 1

这种行为的原因在于Go的调度器(在golang-nuts上有一个更简短的问题描述)。上述的goroutine在同一时间点开始执行(根据时间推断,再加上对startTime变量内存位置的检查证明时间对象并没有被“回收”),但一旦它们遇到http.Get()函数,就会被调度出去。时间递增是因为http.Get()创建了一个瓶颈,不允许并发执行生成的大量goroutine。这里似乎使用了一种FIFO队列。

推荐观看和阅读:
解释Golang I/O多路复用netpoller模型
队列、公平性和Go调度器

通过调整waitgroup大小,我发现一些值显示出更一致的时间(而不是递增的时间)。所以我想知道waitgroup大小对总体和个别时间的影响是什么。我将上述代码重构为一个程序,在给定范围内的每个waitgroup大小进行多次实验,并将每次运行的总体时间和个别时间持久化到SQLite数据库中。生成的数据集可以轻松地在Jupyter Notebook等环境中使用。根据当前的设置,我很遗憾只能在被限制之前完成约40K个请求。如果你有兴趣但不想等待数据,可以在我的GitHub上找到一些数据集,因为完成需要相当长的时间。有趣的结果是,对于小的waitgroup大小,同时/顺序比率急剧下降,并且在连接开始被限制的地方可以看到。这次运行在那时被手动中止。

并发运行时间/顺序运行时间与waitgroup大小的比率:
goroutine中time.Now()的意外行为

不同waitgroup大小的个别时间的图表。

goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为

// 这里是代码部分,不需要翻译
英文:

The cause of this behaviour lies in the scheduler of Go(shorter version of this question at golang-nuts). The above goroutines all start execution at the same point in time (as the timings suggest, plus inspection of the memory location for startTime variable proves that the time object is not "recycled" ), but are descheduled once they hit http.Get(). The timings are incremental because the http.Get() creates a bottleneck that does not allow for concurrent execution of the amount of goroutines spawned. It seems some sort of FIFO queue is used here.
Recommended watching and reading:
Explaining the Golang I/O multiplexing netpoller model
Queues, Fairness and the Go scheduler

Playing around with waitgroup sizes I found some values that showed much more consistent timings (insted of incremental). So I was wondering what the impact of waitgroup size on total and individual timings is. I refactored above into a program that does a number of experiments per waitgroupsize in a given range, and persists total and individual timings per run to an sqlite database. The resulting dataset can be easily used in e.g. a Jupyter Notebook. With the current settings I have unfortunately only been able to get to about 40K requests before getting throttled. See my github for some datasets if you are interested but do not want to wait for data, as it takes quite long to finish. Fun result, steep decline in ratio concurrent/sequential for small wg sizes and you see at the end where the connection starts getting throttled. This run was aborted manually at that time.
Concurrent running time / sequential running time vs wait group size:
goroutine中time.Now()的意外行为

Some plots of the individual timings for different waitgroup sizes.

goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为
goroutine中time.Now()的意外行为

package main

import (
	&quot;database/sql&quot;
	&quot;fmt&quot;
	&quot;log&quot;
	&quot;net/http&quot;
	&quot;os&quot;
	&quot;path/filepath&quot;
	&quot;runtime&quot;
	&quot;sync&quot;
	&quot;time&quot;

	_ &quot;github.com/mattn/go-sqlite3&quot;
)

///// global vars
const REQUESTS int = 100           // Single run size, performed two times (concurrent and sequential)
const URL string = &quot;SET_YOUR_OWN&quot; // Some file on a CDN somewhere; used for the GET requests
const DBNAME string = &quot;netRand.db&quot; // Name of the db file. Saved next to the executable
const WGMIN int = 1                // Start range for waitgroup size (inclusive)
const WGMAX int = 101              // Stop range for waitgroup size (exclusive)
const NREPEAT int = 10             // Number of times to repeat a run for a specific waitgroup size

//// types
type timingResult struct {
	// Container for collecting results before persisting to DB
	WaitgroupSize       int
	ConcurrentTimingsMs [REQUESTS]int64
	ConcurrentTotalMs   int64
	SequentialTimingsMs [REQUESTS]int64
	SequentialTotalMs   int64
}

//// main
func main() {
	db := setupDb()
	defer db.Close()
	for i := WGMIN; i &lt; WGMAX; i++ {
		// waitgroup size range
		for j := 0; j &lt; NREPEAT; j++ {
			// repeat for more data points
			timings := requestTimes(i)
			persistTimings(timings, db)
			fmt.Printf(&quot;\n======== %v of %v ============\n&quot;, j+1, NREPEAT)
			fmt.Printf(&quot;current waitgroup size: %v\n&quot;, i)
			fmt.Printf(&quot;max waitgroup size: %v\n&quot;, WGMAX-1)
		}
	}

}

func requestTimes(waitgroupSize int) timingResult {
	// do NTIMES requests in go routines with waitgroupSize
	// do NTIMES requests sequentially

	timings_concurrent, total_concurrent := concurrentRequests(waitgroupSize)
	timings_sequential, total_sequential := sequentialRequests()

	return timingResult{
		WaitgroupSize:       waitgroupSize,
		ConcurrentTimingsMs: timings_concurrent,
		ConcurrentTotalMs:   total_concurrent,
		SequentialTimingsMs: timings_sequential,
		SequentialTotalMs:   total_sequential,
	}

}
func persistTimings(timings timingResult, db *sql.DB) {
	persistRun(timings, db)
	currentRunId := getCurrentRunId(db)
	persistConcurrentTimings(currentRunId, timings, db)
	persistSequentialTimings(currentRunId, timings, db)
}
func concurrentRequests(waitgroupSize int) ([REQUESTS]int64, int64) {
	start := time.Now()

	var wg sync.WaitGroup
	var timings [REQUESTS]int64
	ch := make(chan int64, REQUESTS)

	for i := range timings {
		wg.Add(1)
		go func() {
			defer wg.Done()
			doGetChannel(URL, ch)
		}()
		// waitgroupsize is controlled using modulo
		// making sure experiment size is always NTIMES
		// independent of waitgroupsize
		if i%waitgroupSize == 0 {
			wg.Wait()
		}
	}
	wg.Wait()
	close(ch)

	count := 0
	for ret := range ch {
		timings[count] = ret
		count++
	}

	return timings, time.Since(start).Milliseconds()
}
func doGetChannel(address string, channel chan int64) {
	// time get request and send to channel
	startSub := time.Now().UnixMilli()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	stopSub := time.Now().UnixMilli()
	delta := stopSub - startSub
	channel &lt;- delta
}
func sequentialRequests() ([REQUESTS]int64, int64) {
	startGo := time.Now()
	var timings_sequential [REQUESTS]int64
	for i := range timings_sequential {
		timings_sequential[i] = doGetReturn(URL)
	}
	return timings_sequential, time.Since(startGo).Milliseconds()
}
func doGetReturn(address string) int64 {
	// time get request without a waitgroup/channel
	start := time.Now()
	_, err := http.Get(address)
	if err != nil {
		log.Fatalln(err)
	}
	duration := time.Since(start).Milliseconds()
	return duration
}

//// DB
func setupDb() *sql.DB {
	//      __________________________runs____________________
	//     |                                                  |
	// concurrent_timings(fk: run_id)         sequential_timings(fk: run_id)
	//
	const createRuns string = `
    CREATE TABLE IF NOT EXISTS runs (
    run_id INTEGER NOT NULL PRIMARY KEY,
    time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    waitgroup_size INTEGER,
    concurrent_total_ms INTEGER,
    sequential_total_ms INTEGER,
    concurrent_sequential_ratio REAL
    );`

	const createSequentialTimings string = `
	CREATE TABLE IF NOT EXISTS sequential_timings (
	run INTEGER,
	call_number INTEGER,
	timing_ms INTEGER,
	FOREIGN KEY(run) REFERENCES runs(run_id)
	);`

	const createConcurrentTimings string = `
	CREATE TABLE IF NOT EXISTS concurrent_timings (
	run INTEGER,
	channel_position INTEGER,
	timing_ms INTEGER,
	FOREIGN KEY(run) REFERENCES runs(run_id)
	);`
	// retrieve platform appropriate connection string
	dbString := getConnectionString(DBNAME)
	db, err := sql.Open(&quot;sqlite3&quot;, dbString)
	if err != nil {
		log.Fatalln(err)
	}
	if _, err := db.Exec(createRuns); err != nil {
		log.Fatalln(err)
	}
	if _, err := db.Exec(createSequentialTimings); err != nil {
		log.Fatalln(err)
	}
	if _, err := db.Exec(createConcurrentTimings); err != nil {
		log.Fatalln(err)
	}
	return db
}
func getConnectionString(dbName string) string {
	// Generate platform appropriate connection string
	// the db is placed in the same directory as the current executable

	// retrieve the path to the currently executed executable
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	// retrieve path to containing dir
	dbDir := filepath.Dir(ex)

	// Append platform appropriate separator and dbName
	if runtime.GOOS == &quot;windows&quot; {

		dbDir = dbDir + &quot;\\&quot; + dbName

	} else {
		dbDir = dbDir + &quot;/&quot; + dbName
	}
	return dbDir
}
func persistRun(timings timingResult, db *sql.DB) {
	tx, err := db.Begin()
	if err != nil {
		log.Fatalln(err)
	}

	insertRun, err := db.Prepare(`INSERT INTO runs(
		waitgroup_size, 
		sequential_total_ms, 
		concurrent_total_ms, 
		concurrent_sequential_ratio) 
		VALUES(?, ?, ?, ?)`)

	if err != nil {
		log.Fatalln(err)
	}
	defer tx.Stmt(insertRun).Close()
	_, err = tx.Stmt(insertRun).Exec(
		timings.WaitgroupSize,
		timings.SequentialTotalMs,
		timings.ConcurrentTotalMs,
		float32(timings.ConcurrentTotalMs)/float32(timings.SequentialTotalMs),
	)
	if err != nil {
		log.Fatalln(err)
	}
	err = tx.Commit()

	if err != nil {
		log.Fatalln(err)
	}
}

func getCurrentRunId(db *sql.DB) int {
	rows, err := db.Query(&quot;SELECT MAX(run_id) FROM runs&quot;)
	if err != nil {
		log.Fatal(err)
	}
	var run_id int
	for rows.Next() {
		err = rows.Scan(&amp;run_id)
		if err != nil {
			log.Fatalln(err)
		}
	}
	rows.Close()
	return run_id
}
func persistConcurrentTimings(runId int, timings timingResult, db *sql.DB) {
	tx, err := db.Begin()
	if err != nil {
		log.Fatalln(err)
	}

	insertTiming, err := db.Prepare(`INSERT INTO concurrent_timings(
		run, 
		channel_position, 
		timing_ms) 
		VALUES(?, ?, ?)`)

	if err != nil {
		log.Fatalln(err)
	}
	for i, timing := range timings.ConcurrentTimingsMs {
		_, err = tx.Stmt(insertTiming).Exec(
			runId,
			i,
			timing,
		)
		if err != nil {
			log.Fatalln(err)
		}
	}

	err = tx.Commit()

	if err != nil {
		log.Fatalln(err)
	}
}
func persistSequentialTimings(runId int, timings timingResult, db *sql.DB) {
	tx, err := db.Begin()
	if err != nil {
		log.Fatalln(err)
	}

	insertTiming, err := db.Prepare(`INSERT INTO sequential_timings(
		run, 
		call_number, 
		timing_ms) 
		VALUES(?, ?, ?)`)

	if err != nil {
		log.Fatalln(err)
	}
	for i, timing := range timings.SequentialTimingsMs {
		_, err = tx.Stmt(insertTiming).Exec(
			runId,
			i,
			timing,
		)
		if err != nil {
			log.Fatalln(err)
		}
	}

	err = tx.Commit()

	if err != nil {
		log.Fatalln(err)
	}
}

huangapple
  • 本文由 发表于 2022年4月4日 20:29:40
  • 转载请务必保留本文链接:https://go.coder-hub.com/71737286.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定