今川館

都内勤務の地味OLです

Goの乱数&time.Durationの算術演算

ランダムに数百ミリ秒スリープしたい

Goで乱数とスリープを組み合わせた処理を書こうとして苦しんだ。

やりたいことは以下なのだが、

  • 100〜900ミリ秒スリープする処理を5回繰り返す
  • 但し、同じ秒数の待機は一度しか許さない(一回100ミリ秒待ったら、もはや100ミリ秒待ってはいけない)
  • 5回繰り返した累積の待機秒数を最後に標準出力に出す。

これは思ったより難しかった。

サンプルコード

まず最初にサンプルコードを示して、ハマりポイントを解説したい。
紆余曲折を経て、以下のコードで期待通りの動きをしてくれた。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	var acc, delay time.Duration
	rand.Seed(time.Now().UnixNano()) // ハマりポイント1
	choices := rand.Perm(9)

	for _, x := range choices[:5] {
		// ハマりポイント2
		delay = time.Duration((x+1)*100) * time.Millisecond
		acc += delay
		time.Sleep(delay)
	}
	fmt.Printf("合計: %v", acc)
}

ハマりポイント1: ランダムにならない(乱数シードの初期化忘れ)

このプログラムを取り敢えず動く状態にして何度か実行していたら、すべて結果が同じ秒数になってしまうことに気づいた。
これは、乱数のシードを適切に初期化しなかったことが原因らしい(以下の記事を参照)。

Random Seed って言葉を初めて知った! | ちょまど帳

こちらはコメント欄が重要
Goで乱数 - Qiita

乱数シードはナノ秒の方が良さそうです。
(1秒以内に実行されたら、同じ結果になる)

サンプルコードの通り、rand.Seed(time.Now().UnixNano())したらちゃんと結果がばらけてくれた。

ハマりポイント2: time.Durationをうまく扱えなくて苦戦

既に他の記事に書いたことだが、time.Sleepは引数にtime.Durationを取る。

これの算術演算に苦戦した。

delay = time.Duration(100 * (x+1) * time.Millisecond)

これができないので、以下の通り、先にtime.Duration(100 * x)してからtime.Millisecondと乗算する。

time.Duration((x+1)*100) * time.Millisecond

どうも、数値のリテラルと掛け算する場合は暗黙にtime.Duration同士に型を推測してくれるようだが、リテラルではなく変数だとダメみたいだ。

当然、変数accもtime.Durationで宣言しておかないとダメ。

var acc int
var delay time.Duration

(中略)
acc += delay
// これはコンパイルエラー
// invalid operation: acc += delay (mismatched types int and time.Duration)

両方ともtime.Durationで宣言しておくこと。
もしaccを整数として扱いたい場合は、型をキャストして使う。以下はOK。

fmt.Printf("合計: %v, 整数-> %v", acc, int(acc)+1000) // 合計: 2.8s, 整数-> 2800001000