はじめに
最近Goのlogパッケージの記事を読んでいたらパイプライン(| <- これ)が+のように中間演算子として使われているのを見て「なにその演算子?」となったので調べて記事にしました。
基本的に記事のコードはGoです。
問題のコード
log.SetFlags(log.LstdFlags | log.Lshortfile)
ちなみにlog.LstdFlagsとlog.Lshortfileはどちらもint型です(具体的な実装はあとにあります)。
公式ドキュメントにはどう書かれていたか
Goの公式言語仕様には + や - などと一緒に演算子の一つとして載っています。
+ sum integers, floats, complex values, strings
- difference integers, floats, complex values
* product integers, floats, complex values
/ quotient integers, floats, complex values
% remainder integers
& bitwise AND integers
| bitwise OR integers
^ bitwise XOR integers
&^ bit clear (AND NOT) integers
<< left shift integer << unsigned integer
>> right shift integer >> unsigned integer
ですがこのドキュメントでは | は + や - と同様に、そんなん説明しなくても知ってるやろみたいな感じだったので結局この演算子が何をするものなのか分かりませんでした。その代わりbitwase ORという演算子の名前?的なものをゲットすることが出来ました。
そしてGoogleで"bitwise OR"とかなんとか検索するとbitwise ORというのはbit演算子の1つだということが分かりました。
bit演算子とは
bit -> bit演算 -> bit演算子という順に調べて分かった事を軽く説明していきます。
そもそもbitって何?
みなさんはご存知だと思いますがコンピュータは小さなon/offスイッチの集合で出来ています。
このスイッチのことをbitといい、それを8個ずつグループにしたものをbyteといいます。
bitだけではon offの2種類の数字 == 0と1しか表現出来ませんが、byteでは8桁の二進数として表現できます。これによってコンピュータは大きな数字の計算ができるわけですね。
00000000のように表記し、右端から0bit目、1bit目と数えていきます。
bit演算って何をするの?
スイッチのon/offを制御するのがbit演算です。
基本的に8の倍数個をまとめて、つまりbyte単位で処理を行います。
bit演算はbit演算子を使って行います。
bit演算子ってどんなものがあるの?
ここでは後の"logパッケージを読んで理解を深める"チャプターで使う4つの演算子について説明します。
| (bitwise OR)
この記事のメインの演算子である | (bitwise OR)は二つのスイッチを比較してどちらかが1であれば1、どちらも0であれば0を返します。
e.g.
c = a | b
a: 01010101
b: 11001100
c: 11011101
& (bitwise AND)
bitwise ORの対の演算子である & (bitwise AND)は二つのスイッチを比較してどちらも1であれば1、そうでなければ0を返します。
e.g.
c = a & b
a: 01010101
b: 11001100
c: 01000100
<< (left shift) と >> (right shift)
<< (left shift)はスイッチのon/offの状態を一つ左に、>> (right shift)は一つ右に動かします。
ずらして空いた枠には0が入り、決められた桁数を超えた場合にははみ出た部分が消えます。消える事をオーバーフローといいます。
e.g.
c = a << 1
a: 10101010
c: 01010100
---
c = b >> 1
b: 11001100
c: 01100110
Example
少しの説明だけでは分かりづらいかな、と思ったのでPlaygroundにサンプルコードを書きました。
説明分かりづらいわ!って人はこっちを見た方が理解が早いかもしれません。
https://play.golang.org/p/wq8Dr7_SpMN
logパッケージを読んで理解を深める
このチャプターではGoの標準パッケージであるlogパッケージを読んでbit演算子の具体的な使い方を学びます。
Go言語ではbit演算子はint型の数字に対して使う演算子であり、とりあえずこの記事ではbit演算子を数字を二進数(bitの集合)として扱うための演算子として認識しています(分かりやすくするため&筆者の知識不足のため)。
今回は下記のコードを実行したとして読み進めていきます。
package main
import "log"
func main() {
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.Println("hoge")
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("fuga")
}
logパッケージではbit演算を使ってフラグの管理を行っています。
では実際のコードを見ていきましょう。
まずlog.SetFlagsで引数に渡しているlog.LstdFlags、log.Lshortfileなどのフラグの定義から見ていきます。
フラグの定義
const (
Ldate = 1 << iota // the date in the local time zone: 2009/01/23 |
Ltime // the time in the local time zone: 01:23:23 |
Lmicroseconds // microsecond resolution: 01:23:23.123123. assumes Ltime. |
Llongfile // full file name and line number: /a/b/c/d.go:23 |
Lshortfile // final file name element and line number: d.go:23. overrides Llongfile |
LUTC // if Ldate or Ltime is set, use UTC rather than the local time zone |
Lmsgprefix // move the "prefix" from the beginning of the line to before the message |
LstdFlags = Ldate | Ltime // initial values for the standard logger |
)
これがフラグの定義です。iotaというのはconst識別子で連番の整数を生成します。iotaは0から始まるのでLdateは1 << 0、Ltimeは1 << 1のように続いていきます。
1は二進数で表すと00000001なのでそこからどんどんleft shiftでずらしていくとこんな感じになります。
| 定数名 | 値 |
|---|---|
| Ldate | 00000001 |
| Ltime | 00000010 |
| Lmicroseconds | 00000100 |
| Llongfile | 00001000 |
| Lshortfile | 00010000 |
| LUTC | 00100000 |
| Lmsgprefix | 01000000 |
| LstdFlags | 00000011 |
フラグの値を把握できたら、次はそれがどのようにlog.Printlnに適応されるのかを見ていきましょう。
フラグの適応
全ての分岐を読むのは面倒なので実装の一部を切り取って説明したいと思います。
ここではhogeの方を評価していると思って読んでください。
if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {
if l.flag&LUTC != 0 {
t = t.UTC()
}
if l.flag&Ldate != 0 {
year, month, day := t.Date()
itoa(buf, year, 4)
*buf = append(*buf, '/')
itoa(buf, int(month), 2)
*buf = append(*buf, '/')
itoa(buf, day, 2)
*buf = append(*buf, ' ')
}
if l.flag&(Ltime|Lmicroseconds) != 0 {
hour, min, sec := t.Clock()
itoa(buf, hour, 2)
*buf = append(*buf, ':')
itoa(buf, min, 2)
*buf = append(*buf, ':')
itoa(buf, sec, 2)
if l.flag&Lmicroseconds != 0 {
*buf = append(*buf, '.')
itoa(buf, t.Nanosecond()/1e3, 6)
}
*buf = append(*buf, ' ')
}
}
Ldate, Ltime, Lmicrosecondsフラグを評価しているif文です。
l.flagというフィールドにlog.SetFlagsでセットしたフラグが入ってます。
では最初の分岐を見ていきましょう。
if l.flag&(Ldate|Ltime|Lmicroseconds) != 0 {...
ここで評価している式について詳しく見ます。
この式はLdate、Ltime、Lmicrosecondsフラグが与えられているか判定しています。
l.flagにはLstdFlags | Lshortfileが入っているのでbitで表すと00010011となり、
Ldate|Ltime|Lmicrosecondsを上の表を見て計算すると00000111となります。
よってbitにすると00010011 & 00000111の形になります。
&を計算すると l.flagは00000011(十進数に直すと3)になり、この分岐を進むことになります。
二番目の分岐
if l.flag&LUTC != 0 {...
二番目の分岐はLUTCフラグが与えられているかを判定しています。
l.flagは00010011、LUTCは上の表から00100000なので&を計算すると00000000が返ってきます。
00000000は十進数に直しても0なのでこの分岐は進みません。
セットしていないフラグの評価はしっかり防げていますね。
三番目以降の分岐は二番目以前と大体同じなので割愛しますが、その後もしっかりセットしたフラグは評価され、セットしていないフラグは評価されないようになっています。
このチャプターの最初のコードを実行してみるとこのようになります。
2009/11/10 23:00:00 prog.go:7: hoge
2009/11/10 23:00:00.000000 fuga
hogeの方ではprog.go:7のようにファイルのどこで実行したかと日時が確認でき、Lshortfileと日時を確認できるLstdFlagsが評価されていることが
fugaの方では時刻の表記が23:00:00.000000とmsまで表示されるようになっており、Lmicrosecondsが評価されていることが分かりますね。
まとめ
今回はGoのコードで説明しましたがbit演算はGo以外のほとんど言語で実行できます。
フラグ管理にぜひbit演算を活用してみてはいかがでしょうか?
この記事では具体的な使い道としてフラグ管理を取り上げましたが、他にもたくさん使い道があるようなので気になる方はこの記事を読んでみるといいと思います!
ビット演算 (bit 演算) の使い方を総特集! 〜 マスクビットから bit DP まで 〜 - Qiita
あとこの記事の中でなにか間違っていることが書いてあったら遠慮せずにコメントしたり修正リクエストを送っていただきたいです。よろしくお願いします!
参考
Go Tips連載8: logパッケージでログ出力している場所の情報を出す | フューチャー技術ブログ
The Go Programming Language Specification - The Go Programming Language
ビット演算入門 - Qiita
logパッケージのソースコード