- 投稿日:2019-10-20T21:55:19+09:00
C から Go へコードを移植してハマった話 (そして言語仕様へ)
背景
我々プログラマは、しばしば異なる言語処理系の間において、ソースコードを移植することがあります。
syntax (文法) については、compiler が保証してくれるので、ハマることはあまりありません。しかし、evaluation (評価) については、同じような syntax において、
言語ごとに評価結果が異なることがあり、時々ハマることがあるように思います。今回は、C から Go へ移植する際に、言語仕様についての差異によって、
意図した結果を得るのにややハマった話を書きます。「ぐぬぬ…どう見ても同じように移植したのに…」
例えば、こんな C コードを、
main.c// This is main.c #include <stdio.h> void main () { unsigned char a; unsigned char b; unsigned short c; a = 0x12; b = 0x34; c = 0x0000; c |= (unsigned short)(a << 8); c |= (unsigned short)(b << 0); printf("c is 0x%04X\n", c); }こんな Go のコードへ移植しました。
main.gopackage main import "fmt" func main() { var a uint8 var b uint8 var c uint16 a = 0x12 b = 0x34 c = 0x0000; c |= uint16(a << 8) c |= uint16(b << 0) fmt.Printf("c is 0x%04X\n", c) }二つの変数のビットを結合する、よくある手続きです。
「よし、C のキャストもちゃんと同じように書いたし、
どこからどう見ても全く同じコードやからうまくいくはずや!!」実行しました。
$ gcc -o main.exe main.c $ main.exe c is 0x1234$ go build -o main.exe $ main.exe c is 0x0034「おや…、実行結果が異なるぞ…??」
「移植元の C ソースはキャストもちゃんと書いてあるし、
Go はそもそも型やらキャストやらちゃんとしてないと怒られるはずや…。
わからん、わからんぞ…」後になって思えば、当然のことなのですが (実際はもっと複雑なプログラムでしたが)、
私はコレで30分程度首をひねってしまいました。「ん…?? これは…」
30分首をひねっていると(実際には必死でデバッグしていましたが)…、
「あ、C のこの
a << 8
は、int に拡張されるんやった」
「C のプログラムがうまくいくのは、オーバフローして暗黙に拡張されてるからや…」「でも、ナウい Go にはそんな暗黙の動作なんてないんや…」
main.cc |= (unsigned short)(a << 8); c |= (unsigned short)(b << 0);そうです。
C 言語は、言語が誕生した時代背景や、
言語処理系が CPU へ依存した規格となっていることのせいで、
「未定義動作」や、暗黙の型キャスト、整数の拡張などを頻繁に行います。結局、これらは C も Go もどちらもよくないコードになっていて、
上記のような思い込みから、Go のコードが動かない理由に気づくのが遅れてしまいました。つまり、問題の箇所は、それぞれ以下のように書かなければなりませんでした。
main.cc |= (unsigned short)a << 8; // 8bit 変数を `<<` 演算が評価される前に 16bit 変数へ型キャスト c |= (unsigned short)b << 0;main.goc |= uint16(a) << 8 // 8bit 変数を `<<` 演算が評価される前に 16bit 変数へ型キャスト c |= uint16(b) << 0これで、無事二つのプログラムは等しく評価されるようになりました。
$ gcc -o main.exe main.c $ main.exe c is 0x1234$ go build -o main.exe $ main.exe c is 0x1234言語仕様を確かめる
というわけで、ここまでは、私の記憶で対処をしてきました。
そもそも、それぞれの言語仕様はどうなっているのでしょうか。今回は実際に使用した処理系 (
C
,Go
) の言語仕様に、
今回のような処理が行われる場合、どのようにふるまうと定義されているのかを見てみます。C
以下は、ISO/IEC 9899 (C 言語の国際規格:C99)より参照した、shift 演算の仕様です。
# Bit shift の動作について 6.5.7 Bitwise shift operators (中略) Semantics 3 The integer promotions are performed on each of the operands. The type of the result is that of the promoted left operand. If the value of the right operand is negative or is greater than or equal to the width of the promoted left operand, the behavior is undefined. (筆者訳) << >> 右オペランドが、「拡張された」左オペランドのビット幅を超える場合、 また、第二オペランドが負の値である場合、動作は未定義となります。 # 整数拡張について If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. (筆者訳) int が元の型のすべての値を表すことができる場合、値は int に変換されます。 それ以外の場合は、unsigned int に変換されます。 これらは "integer promotion" (整数拡張) と呼ばれます。以下は、Go の referenceより参照した、shift 演算の (整数値の演算の) 仕様です。
Go
Integer overflow For unsigned integer values, the operations +, -, *, and << are computed modulo 2n, where n is the bit width of the unsigned integer's type. Loosely speaking, these unsigned integer operations discard high bits upon overflow, and programs may rely on "wrap around". (筆者訳) (前略) 符号なし整数演算は、オーバーフローした場合上位ビットを切り捨て、「ラップアラウンド」します。 (8bit 整数において、 0xFF + 0x01 = 0x00 となるということ)やはり、shift 演算において、overflow した場合、
C は暗黙に整数拡張するとされているのに対して、Go は暗黙の動作はなく明確に動作を定義しています。まとめ
時折、better C とも言われる Go ですが、今回の例のように、実際の挙動には構文レベルでは明示的にならないものもあります。
そのようなときは、一次的なドキュメントが読みやすい形で提供されているならば、
言語仕様を参照するのが、手っ取り早く、確実で、かつ言語仕様への理解を深めることにつながるとおもいました。reference
open-std.org ISO/IEC 9899:TC3
The Go Programming Language Specification
- 投稿日:2019-10-20T20:16:04+09:00
go1.13でgolangci-lint使った時にエラーが多発する時の対処法
TL;DR
GO111MODULE=off go get -u github.com/golangci/golangci-lint/cmd/golangci-lintもしくは
v1.18.0
以上を使用するエラー内容
goのバージョンを1.13にした後に、
golangci-lint
を使用した時に以下のようなエラーが多発しました。internal/domain/hogehoge.go:1: /usr/local/Cellar/go/1.13.3/libexec/src/path/filepath/match.go:215:15: DecodeRuneInString not declared by package utf8 (typecheck) internal/adaptor/hogege/gehoho.go:1: /Users/xxxxxx/go/pkg/mod/github.com/golang/protobuf@v1.3.2/proto/text_parser.go:255:15: DecodeRuneInString not declared by package utf8 (typecheck)原因
直接的な原因は調べていないので分かりませんが、Go1.12以前の
golangci-lint
を使ってGo1.13以降のコードに対してlintを行うとtypeの違いから、上のエラーが出るようです。
解決するにはgo1.13でビルドされたgolangci-lint
を使用することで回避できます参考:
https://github.com/golangci/golangci-lint/issues/658#issuecomment-528126696
- 投稿日:2019-10-20T15:50:26+09:00
初心者のためのGo言語基礎〜基本文法〜
初心者のためのGo言語基礎〜基本文法〜
初心者のためのGo言語基礎〜Goの特徴とインストール〜
上記に続いて基本文法を簡単にまとめます。1. パッケージ
ソースコードはパッケージの記述から始まります。
hello.gopackage main 〜Goでは、mainパッケージのmain()関数が最初に実行されます。
2. インポート
別のパッケージをインポートします。
下記では「fmtパッケージ」を取り込んでいます。
hello.gopackage main import "fmt" 〜複数パッケージを取り込む時は、以下のような書き方もできます。
hello.gopackage main import ( "fmt" "strings" ) 〜メソッドのpublicとprivate
パッケージ名 + ドット + 関数名 で取り込んだパッケージの関数を使用できます。
Goではpublicメソッド(他のパッケージからも見える関数)は頭文字が大文字になります。
privateメソッドは頭文字が小文字になります。fmt.Println("Hello World!")オプション
importにはオプションも指定できます。
- ドット
- パッケージ名を省略して関数名だけで使える
- 文字列
- 指定のパッケージ名で取り込める
- アンダースコア
- そのパッケージが未使用でもエラーにならない
- 通常Goでは未使用のパッケージが1つでもあるとコンパイルエラーとなる
import ( . "fmt" // 関数名だけで使える str "strings" // パッケージ名strで取り込む _ "math" // 未使用でもOK ) func main() { Println("Hello World!") Println(str.Contains("Hello World!", "Hello")) }
途中
型
関数
変数
基本構文
- 投稿日:2019-10-20T15:50:26+09:00
初心者のためのGo言語基礎〜基本文法①〜
初心者のためのGo言語基礎〜基本文法〜
初心者のためのGo言語基礎〜Goの特徴とインストール〜
上記に続いて基本文法を簡単にまとめます。1. パッケージ
ソースコードはパッケージの記述から始まります。
hello.gopackage main 〜Goでは、mainパッケージのmain()関数が最初に実行されます。
2. インポート
別のパッケージをインポートします。
下記では「fmtパッケージ」を取り込んでいます。
hello.gopackage main import "fmt" 〜複数パッケージを取り込む時は、以下のような書き方もできます。
hello.gopackage main import ( "fmt" "strings" ) 〜メソッドのpublicとprivate
パッケージ名 + ドット + 関数名 で取り込んだパッケージの関数を使用できます。
Goではpublicメソッド(他のパッケージからも見える関数)は頭文字が大文字になります。
privateメソッドは頭文字が小文字になります。fmt.Println("Hello World!")オプション
importにはオプションも指定できます。
- ドット
- パッケージ名を省略して関数名だけで使える
- 文字列
- 指定のパッケージ名で取り込める
- アンダースコア
- そのパッケージが未使用でもエラーにならない
- 通常Goでは未使用のパッケージが1つでもあるとコンパイルエラーとなる
import ( . "fmt" // 関数名だけで使える str "strings" // パッケージ名strで取り込む _ "math" // 未使用でもOK ) func main() { Println("Hello World!") Println(str.Contains("Hello World!", "Hello")) }関数
引数
- 引数の後ろに型を記述する。
- 複数同じ型ならカンマ区切りでまとめられる
func hoge(a,b int) { }戻り値
- 戻り値の方は引数の次に記述する
- 関数は複数の戻り値を返せる
- returnする順番で返す値の順番が決まる
- アンダースコアで戻り値を無視できる
func hoge() (int, int) { return 1, 2 } func main() { x,y := hoge() // xに1、yに2が代入 x,_ := hoge() // 第二戻り値を無視する。 xに1が代入 fmt.Println(x) // x=1 fmt.Println(y) // y=2 }名前付きの戻り値
- goでは戻り値に名前を付けられる
- 関数内ではゼロ値で初期化された変数の扱い
- return明示が必要なく、その時点での値が返される
func main() (a int, b int, err error) { a = 1 return // a=1,b=0,err=nil が返される }関数リテラル
- 無名関数を作成可能
func main() { func(x,y int) int { return x+y // 3が返される }(1, 2) // 引数 }
- 変数に関数を代入することもできる
var sum := func(x,y int) int { return x+y } func main() { fmt.Println( sum(1, 2) ) // 3が表示される }
- 投稿日:2019-10-20T15:32:03+09:00
vscode + golangでgo getをする際のメモ(自分用)
完全に私用のメモ
以下のツールを一括で取得するコマンド
- gocode
- gopkgs
- go-outline
- go-symbols
- guru
- gorename
- gotests
- gomodifytags
- impl
- fillstruct
- goplay
- godoctor
- dlv
- gocode-gomod
- godef
- goreturns
- golint
go get -u -v github.com/mdempsky/gocode github.com/uudashr/gopkgs/cmd/gopkgs github.com/ramya-rao-a/go-outline github.com/acroca/go-symbols golang.org/x/tools/cmd/guru golang.org/x/tools/cmd/gorename github.com/cweill/gotests/... github.com/fatih/gomodifytags github.com/josharian/impl github.com/davidrjenni/reftools/cmd/fillstruct github.com/haya14busa/goplay/cmd/goplay github.com/godoctor/godoctor github.com/go-delve/delve/cmd/dlv github.com/stamblerre/gocode github.com/rogpeppe/godef github.com/sqs/goreturns
- 投稿日:2019-10-20T14:57:59+09:00
初心者のためのGo言語基礎〜Goの特徴とインストール〜
初心者のためのGo言語基礎〜Goの特徴とインストール〜
最近Goを学習し始めたので、基礎から少しずつ学んだことをアウトプットしていきます。
システムレイヤーも含めて学んでいるので、基本的な文法から低レイヤーの仕組みまで少しずつまとめていきたいです。
1. Go言語とは
GoはGoogleによって開発されたプログラミング言語です。
GolangやGo言語とも呼ばれます。
2009年に発表され、最近ではWebサービスのサーバサイド開発に多く使われるようになりました。人気急上昇中で年収も高い
株式会社ビズリーチが発表した「プログラミング言語別年収ランキング2018」では 第1位 となっており、需要が高く年収が高い傾向にあるようです。
様々な開発に使われている
すでにGoは様々なソフトウェア開発で使われています。
あのDockerもGoで書かれていたり、身近なサービスだとメルカリやグノシーでも使われているようです。2. Go言語の特徴
2.1. 言語仕様がシンプル
言語仕様がシンプルなため、初心者でも習得しやすい言語と言われています。
C言語の性能と、Pythonの書きやすさを併せ持った言語として開発された背景があります。
機能をシンプルにすることによって、予期せぬミスを防いだり高いパフォーマンスを実現しています。例えば以下の特徴があります。
- 繰り返し構文はfor文のみ(whileは無い)
- if文の{}は省略できない
- if文の三項演算子は使えない
- ジェネリクスがない
- 継承がない
- 例外処理がない2.2. パフォーマンスが高い
C言語の性能を目指して作られただけあり、非常に動作が高速です。
- コンパイルが速い
- 実行速度が速い
- 消費リソースが少ない
- 拡張性が高い
- 並列処理が得意
2.3. 大規模開発にも向いている
- 静的型付け言語である
- コーディング規約が定められている
go fmt
という標準コマンドにより、統一的にフォーマットされる- 誰が書いても読みやすい記述になる
3. Go言語をインストールする
※macOSを前提
以下の公式サイトからダウンロードし、インストールしてください。
https://golang.org/dl/以下のコマンドを入力し、バージョン番号が表示されればインストール成功です。
go version
4. Go言語でHello World!
新しい言語を学習する時はまずはコレですね。
hello.gopackage main import "fmt" func main() { fmt.Println("Hello World!") }実行します。へろーわーるど
$ go run hello.go Hello Wolrd!
5. Goの基本構文を学習したい
A Tour of Goでサラッと基本を学びましょう。
- 投稿日:2019-10-20T00:28:05+09:00
goでエラーなのか正常系なのかを判断して出力するログ
目的
goで製品開発していて単純なlog.Printfよりはもう少し賢い簡単なログライブラリが必要になりました。そこで要求事項を整理した結果、まずはログの内容がinformationなのかerrorなのかをprefixで判別できるものを作ろうとなり、 [INFO] [ERROR] とログの頭につく形でログを出力することを考えました。
例えばこういうライブラリ関数になります
log.info("this is information")
log.error("this is error")
出力
- [INFO] this is information
- [ERROR] this is error
困ったこと
結果としては上の出力が得られれば問題はなかったのですが以下のような問題が出ました
・すでに結構な数のログが製品中に埋め込まれている。これらを全てどれがinfoかどれがerrorかを一つずつ見ながら対応する関数呼び出しに置き換えるのは大変
・今後ログ関数を呼び出す場合にプログラマ全員がきちんと該当するレベルのエラーを呼び出してくれるのか不安。またコードレビューの際にもログレベルが妥当かをいちいち見て指摘しなければいけない解決策
ログの呼び出し内容にerrorが含まれていればprefixをERRORに、それ以外ならINFOにすれば自動で振り分けられるのではないか。ということで以下が実装になります。
func logger(format string, args ...interface{}) { prefix := "[INFO] " if args == nil { format = prefix + format log.Printf(format) } else { for _, arg := range args { switch arg.(type) { case error: prefix = "[ERROR] " break } } format = prefix + format log.Printf(format, args...) } } func main(){ logger("this is information") logger("%s","yet another information") logger("%v",errors.New("i am error")) }出力
2019/10/20 00:11:20 [INFO] this is information
2019/10/20 00:11:20 [INFO] yet another information
2019/10/20 00:11:20 [ERROR] i am errorまとめ
何も考えずに、とりあえずこの関数呼び出してログ出力して貰えば適切に動いてくれる、十分製品として実用レベルのログライブラリとして活躍しました。
- 投稿日:2019-10-20T00:18:40+09:00
Go で int スライスの二分探索
Go で int スライスの二分探索
Go 言語でスライスの二分探索をするには sort.Search を使う.
int スライスについては、Python の bisect_left 相当の sort.SearchInts があるのでそれを使えば良い. bisect_right 相当はないので sort.Search で書くことになる.
package main import ( "fmt" "sort" ) func bisectLeft(a []int, x int) int { return sort.SearchInts(a, x) } func bisectRight(a []int, x int) int { return sort.Search(len(a), func(i int) bool { return a[i] > x }) } func main() { a := []int{1, 2, 2, 3} fmt.Println(bisectLeft(a, 0)) fmt.Println(bisectRight(a, 0)) fmt.Println(bisectLeft(a, 2)) fmt.Println(bisectRight(a, 2)) fmt.Println(bisectLeft(a, 4)) fmt.Println(bisectRight(a, 4)) }出力結果
0 0 1 3 4 4比較の度に関数呼び出しをするためこの実装はすこぶる遅い. 以下の実装は倍くらい速い.
func bisectLeft(a []int, x int) int { lo := 0 hi := len(a) for lo < hi { mid := (lo + hi) / 2 if a[mid] < x { lo = mid + 1 } else { hi = mid } } return lo } func bisectRight(a []int, x int) int { lo := 0 hi := len(a) for lo < hi { mid := (lo + hi) / 2 if x < a[mid] { hi = mid } else { lo = mid + 1 } } return lo }