Goでビットを利用したフラグ管理を行う実装パターン

February 14, 2021

今回はGoでビットを利用してフラグ管理を行う際の実装パターンを紹介します

go/toolsのssa.BuilderModeはこの方法を用いて実装されています。

(というかこのBuilderModeの実装を読んでもらえればこの記事で紹介することは終わりです)

実装を見てみよう

type BuilderMode uint

const (
	PrintPackages        BuilderMode = 1 << iota // Print package inventory to stdout
	PrintFunctions                               // Print function SSA code to stdout
	LogSource                                    // Log source locations as SSA builder progresses
	SanityCheckFunctions                         // Perform sanity checking of function bodies
	NaiveForm                                    // Build naïve SSA form: don't replace local loads/stores with registers
	BuildSerially                                // Build packages serially, not in parallel.
	GlobalDebug                                  // Enable debug info for all packages
	BareInits                                    // Build init functions without guards or calls to dependent inits
)

こんな感じの実装になってます、modeそれぞれの意味は読まなくて大丈夫です。 Goでenumを作成するときにお馴染みのiotaが出てきていますね。

The value of iota is reset to 0 whenever the reserved word const appears in the source (i.e. each const block) and incremented by one after each ConstSpec e.g. each Line. This can be combined with the constant shorthand (leaving out everything after the constant name) to very concisely define related constants.

というやつです、詳しくはこちら

iotaによって、上記のBuilderModeは2進数で表すと

1
10
100
1000
10000
...

というふうに定義されていきます。それぞれの桁で各モードのON/OFFが表現されることになりますね。

modeの管理を行う関数を定義する

BuilderModeの指定を行う際に一々計算をしているとめんどくさく分かりづらいです。BuilderModeの実装にはビット和の演算を用いたSetメソッドが生えています。 実装を見てみましょう

// Set parses the flag characters in s and updates *m.
func (m *BuilderMode) Set(s string) error {
	var mode BuilderMode
	for _, c := range s {
		switch c {
		case 'D':
			mode |= GlobalDebug
		case 'P':
			mode |= PrintPackages
		case 'F':
			mode |= PrintFunctions
		case 'S':
			mode |= LogSource | BuildSerially
		case 'C':
			mode |= SanityCheckFunctions
		case 'N':
			mode |= NaiveForm
		case 'L':
			mode |= BuildSerially
		case 'I':
			mode |= BareInits
		default:
			return fmt.Errorf("unknown BuilderMode option: %q", c)
		}
	}
	*m = mode
	return nil
}

string型のsが渡されて、その文字によってフラグがONになっていることがわかります。

また、文字の扱いに関しては以下のように説明があります。

const BuilderModeDoc = `Options controlling the SSA builder.
The value is a sequence of zero or more of these letters:
C	perform sanity [C]hecking of the SSA form.
D	include [D]ebug info for every function.
P	print [P]ackage inventory.
F	print [F]unction SSA code.
S	log [S]ource locations as SSA builder progresses.
L	build distinct packages seria[L]ly instead of in parallel.
N	build [N]aive SSA form: don't replace local loads/stores with registers.
I	build bare [I]nit functions: no init guards or calls to dependent inits.
`

Setメソッドを用いて以下のようにModeの変更が可能です。

mode.Set("D") // GlobalDebug(D)が有効に
mode.Set("DP") // GlobalDebug(D)、PrintPackages(P)が有効に

String()メソッドもはやしておく

func (m BuilderMode) String() string {
	var buf bytes.Buffer
	if m&GlobalDebug != 0 {
		buf.WriteByte('D')
	}
	if m&PrintPackages != 0 {
		buf.WriteByte('P')
	}
	if m&PrintFunctions != 0 {
		buf.WriteByte('F')
	}
	if m&LogSource != 0 {
		buf.WriteByte('S')
	}
	if m&SanityCheckFunctions != 0 {
		buf.WriteByte('C')
	}
	if m&NaiveForm != 0 {
		buf.WriteByte('N')
	}
	if m&BuildSerially != 0 {
		buf.WriteByte('L')
	}
	if m&BareInits != 0 {
		buf.WriteByte('I')
	}
	return buf.String()
}

これによってログなどに出力した時に先程の文字列が出力されるようになっています

	a := BuilderMode(0)
	a.Set("PD")

	fmt.Print(a) // PDが出力される

どう言った場面で有効な手段か

そのままですがboolのoption(フラグ)を複数持たせて振る舞いを変えさせたい場合に有効です。

ただし、一つの関数や構造体などにoptionを実装し、振る舞いを変えまくるより普通に複数の関数/構造体などに作り分けた方がわかりやすい場合もあると思います。

終わりに

今回はビットによるフラグ管理を利用したOptionの作成を紹介しました。知ってる人にとっては、はいはいあれねっていう感じだったかもしれません。

久しぶりに技術記事書いてえらい

このエントリーをはてなブックマークに追加