🐐

go の好きなところと嫌いなところ

2021/07/25に公開1

これは何?

go を書くことがわりとあるんだけど、好きだなと思うところと嫌いだなと思うところがある。

タイトルの通り好き嫌いの話。
その仕様が正しいとか間違っているとかいう話をしたいわけではない。

全般的には、エコシステムが好き。文法はあまり好きではない。

👍 コンパイルが速い

あんまり大規模なコードをビルドしたことはないんだけど、速い印象。
大変ありがたい。

👍 ビルドが簡単

C/C++ だと、処理系によっては別ディレクトリに同名ファイルがあると不幸になったり、依存関係をちゃんとするのが面倒だったりする。go は、そういうのはビルドする仕組みの中に隠されていて、ユーザは単に build するだけでよい。すばらしい。

int の最大値・最小値を得る簡単な方法がない

math.MaxInt32 なんかはあるんだけど、 math.MaxInt はない。
なぜなのか。

むしろ、 uint32 なんかは (1<<32)-1 でよいので定義がなくてもあんまり困らなくて、 int の最小値はパッと書けないので困る。

以下の計算で求まるが、定義しておいてよ、と思う。

maxInt := int((^uint(0))>>1)
minInt := -1-maxInt

条件演算子(三項演算子)がない

C / ruby / JavaScript などで使われている ? : 演算子。

例えば JavaScript なら

JavaScript
let x = cond() ? hoge(1) : fuga(2,3);

こういうふうに書くやつ。
go だと

go
x := func() xtype {
	if cond() {
		return hoge(1)
	}
	return fuga(2,3)
}()

こんなにかさばる。
ちなみに

go
x := condOpForXType(cond(), hoge(1), fuga(2,3))

みたいな使い方をする関数を定義する気にはならない。こうしてしまうと、 cond()false でも hoge(1) が呼ばれちゃうから。

go
x := condOpForXType(cond(), func(){ return hoge(1) }, func(){ return fuga(2,3)})

ならアリだけど、こんなに書くなら最初のでいいや、と思う。

演算子じゃなくて if 式 とかでもいいんだけどとにかくそういうのがないので、気持ちとしては全然大したことをしていないのに6行にもわたるコードを書かされ、読まされるのがつらい。

👍 言語を決めている人たちが書式を決めている

go fmt で言語を決めている人の書式になるところが素晴らしい。
どの書式がより合理的かの議論とか、そういうことをする必要がない。

go fmt が定める書式は正直あんまり気に入らないんだけど、公式が決めた書式が存在することのメリットが気に入らない気持ちを大幅に上回っているので書式の好みなんてとどうでも良い。

書式を度外視して適当に書いて、エディタで保存したら勝手に go fmt が走ってきれいになる、という流れで異論が出ようがないのが素晴らしい。

👍 Windows, Linux, macOS で同じソースで割とちゃんと動きがち

Java ほどじゃないけど、同じソースでわりとちゃんと動きがち。
ネットワーク周りも含めてちゃんと動きがちなので本当に助かる。

Java と違って何もインストールしなくても動くバイナリができるので導入が手軽。
本当に助かる。

👍 クロスコンパイルが簡単

前述の「同じソースで動きがち」とのあわせ技で、macOS 上でテストとビルドまでやって Windows 用のバイナリを作ってもだいたい行ける。

Windows 上でのテストが完全に不要になったりはしないけど、ほぼ完成してから動くことを確認する、ぐらいの感じになる。大変便利。

👍 chan と go routine がすばらしい

すばらしい。
ただただ素晴らしい。

👍 メモリの種類を気にしなくていい

ローカル変数のアドレスを返しても OK だったり。
関数の呼び出しが深くなりすぎたらスタック領域を確保しなおしたり。

C/C++ だと、この変数はスタックにあるからどうだとか、ヒープはたっぷりあるけどスタックはあんまりないから再帰呼び出しだと無理とか、そういうことを意識せざるをえないんだけど、go はよろしくやってくれて素晴らしい。

アドレスが取れたり取れなかったり

構造体・スライス・配列・map のリテラルならアドレスが取れるのに、文字列リテラルや整数リテラルのアドレスが取れない。構造体なんかでも、関数の返戻値だとアドレスが取れない。

go
type hoge struct {
	foo string
}
ptrToStruct := &(hoge{}) // ok
ptrToSlice := &([]int{}) // ok
ptrToString := &("") // error
ptrToSlice2 := &(func() []int { return []int{} }) // error

代わりにどう書けばいいのかはわかっている。一貫性がないのが好きじゃないという話。

全部アドレスが取れればいいのになと思う。

多重代入と変数定義の組み合わせ

a, b := 1, 2
a, c := 3, 4 // [1] 定義済みの a を書き換える
{
	a, d := 5, 6 // [2] 新たな a を導入する
}

ただでさえ外側の変数を内側の変数が隠すという仕様はトラブルになりやすいんだけど、多重代入での初期化と組み合わさってさらにわかりにくくなっている。

上記の [1] は 既存の a を書き換え、 [2] は既存の a を書き換えない。

難しいよね。

generics がない

型をパラメータにした関数や型(のようなもの)がないので、 container.Ringsync.Map が使いにくい。

最大値関数とかも型ごとに用意しなきゃなので辛い。

1.17 beta で使えるようになっているらしい Type Parameter が入れば幸せになるんだと思っているけど、未調査。

ユーザー定義型が組込型のように振る舞えない

a, b, c, xfloat64 なら

t:=a+x*(b+x*c)

と書ける式が、big.Float になった途端、

t := &big.Float{}
t.Copy(x)
t.Mul(t, c)
t.Add(t, b)
t.Mul(t, x)
t.Add(t, a)

となる。書きにくく読みにくい。ユーザー定義型に演算子を生やせないことが原因と思う。

エラー処理

go の選択した作戦では、エラー処理をサボる人が多発するし、サボっていることに気づくのも難しい。
実際 fmt.Println などのエラーちゃんと処理しているサンプルコードはとても少ない。

例外機構を避けたいという気持ちはわかるんだけど、今の go が採用した作戦は好きになれない。

range の気持ちがわからない

slice や map でループを回すときに

go
for ix, element := range(slice) {}

と書くわけだけど、この range の気持ちがわからない。
実用的に困る点として

go
for element := range(slice) {}

という誤記をしがちということがあるけど、それ以上になにか、この range をどう思ったらいいのかわからないという戸惑いがある。

パット見、 range は関数か演算子のように見えるけど、そうじゃないんだよね。

狭化変換とそうでない変換の区別ができない

例えば C++ には

C++
std::int32_t int32func(int);
std::int64_t int64func(int);
std::int32_t hoge = int64func(0); // 狭化変換なので警告
std::int64_t fuga = int32func(1); // 情報は失われないので無警告

上記のように「暗黙の型変換」という様々なトラブルの原因となった機能がある。
情報が失われない方向の暗黙の変換は不幸にはなりにくいので警告せず、情報が失われる方向の暗黙の変換は不幸になりやすいので警告を出す、という対応が可能になっている。

go はこれに対して「そもそも暗黙の変換なんて出来なければいいよね」という対応をしていて、それは悪くないと思うんだけど

go
var hoge int32 = int32(int64func()) // 狭化変換だけど、警告は出ない
var fuga int64 = int64(int32func())

上記のように、狭化変換でも警告は出ない。

かといって、型システムで頑張ってそういう関数を作ることもできない。と思う。
Type Parameter が入ったらできるようになるのかなぁ。

slice の append

slice に要素を追加する方法が

go
slice = append(slice, a...)

しかないのが残念。

なにが残念なのかというと、追加されるスライスを2回書かなきゃいけないところ。

実際たまに、一方だけを書き換えてもう一方を書き換え忘れるというミスをする。

map の要素のメンバへの代入

go
type hoge struct{ a int }
m := map[int]hoge{1: hoge{}}
m[1].a = 2 // cannot assign to struct field m[1].a in map
m[1] = hoge{a: 3} // ok

map の要素型のメンバを一個だけ書き換えたいことだってあるよ。
そういうことをしたければポインタを使えということなのかな。

参照という機能が言語仕様にないからこういう事になったのかなと思ってるんだけど、どうだろう。

👍 ケツカンマ対応

関数の引数までケツカンマ OK な言語は他に知らない。便利。

go
hoge := func(a, b, c int) {}
hoge(1, // first
	2, // second
	3, // third
)

👍 go module

最初存在しなかったけど、 1.11 ぐらいから整備されて、今はバッチリ使える感じで大変便利。

言語公式が定めているので安心感がある。

👍 defer

open と close は必ずペアなんだから毎回 defer f.Close() って書かせるなよ、と思わなくもないけど、シンプルで良いとも思う。

なにかとグローバルな状態を使ってしまうライブラリ

たとえば

go
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)

とか。

go
pi := flag.Int("hoge", 0, "hoge value")
flag.Parse()
fmt.Println(*ip)

とか。

グローバルな状態を使わないやり方もあるのは知っている。
グローバルな状態を使わないやり方だけを提供すればいいのにと思う。

四捨五入も三角関数も float32 に非対応

math パッケージに、色々関数があるんだけど、 float32 を引数としたものはほとんどない。

go の math パッケージを作った人は、「え、今どき単精度使う人なんてほとんどいないでしょ」と思ったんだろうけど、私は単精度の三角関数使いたかったので、え、なんで無いの? と思った。

複数の整数の最大値、整数の絶対値、などが無いのが意外。

この辺りも、generics が無いことが原因かもしれないけど、C++ なら

C++
int a = foo();
int b = bar();
int c = std::max(a,b);
int d = std::abs(c);

で済むところを、 go だと

go
a := foo()
b := bar()
c := func() int {
	if a < b {
		return b
	}
	return a
}()
d := func() int {
	if c < 0 {
		return -c
	}
	return c
}()

などと書くことになる。自作の maxInt とかを定義してもいいけど、定義しておいてよ、と思うし、一個定義しても、え、じゃあ他の整数型は? となる。

ruby や C++ を使っているときに思う「xxx という関数があるかどうか知らないけど、ruby / C++ だからきっと定義されているよね」という感覚が、今の所 go だと全然ない。

日付の書式化の文法が理解しにくい

go
time.Now().Format("2001-02-03 16:05:06") // 間違い

これ考えた人頭いい! みたいな意見も見るけど、全然いい考えだとは思わない。

option 型のようなもの

ある値が存在する場合に限って取り出せるような仕組みがほしい。

Rust の Option みたいなの。
go だと、値があるかもしれない場合はポインタを使うと思うんだけど、ちょっとミスると nil 参照で死んでしまう。

ほしいのは、たとえばこういうやつ。

妄想go
// 取得に成功したら値が、失敗したら空っぽが得られる。
// opt 型は例えば optional[string] みたいな型。
// optional は、 map みたいな、型を作る予約語。
opt := getSomeValue()
if opt -> v { // この -> は if の条件でしか使えない専用の演算子。これで opt から 値を取り出す。
  // opt の中身を得る方法はこれ以外にはない
  doSomething(v)
}

みたいなの。
深く考えているわけではないのでこれではだめかもしれないけど、opt のなかに値が入っていない場合には、その変数のスコープ内に到達出来ないような文法とセットでほしい。
こういう型と文法があれば、 nil で死ぬことがだいぶ減ると思う。

time.Duration などの型

時間を時間で割ったら単位のない値になるはずだけど、そうならない。
不便。

型を単位のように扱いたいのなら、curl みたいに、値に単位をつけられるようにすればいいわけだけど、そうはなってない。

演算子 &^ とその周辺

以前拙記事に頂いたコメント で、 &^ という演算子が存在する理由が書いてあって、その時私はなるほどと思ってしまったんだけど、やっぱり要らないと思う。

^7int になるのが悪い。「下位3ビットがゼロで、残りは全部1」という趣旨の型未定の const になればいい。 ruby で負の数を2進数などで書式化したときに

ruby
"%#b" % -8 #=> "0b..1000"

こうなるんだけど、こういう感じの定数になればいい。

それが int になるのなら -8 になって、 uint16 になったら 0xfff8 になればいい。浮動小数点型にしたり大小比較されたりしたらエラーになればいい。と思う。

それに、 & 以外も使いたいことあるよ。

go
var a uint = 7
log.Println(a &^ 1) // これだけ OK
log.Println(a | ^uint(1)) // a | ^1 だとエラー
log.Println(a ^ ^uint(1)) // a ^ ^1 だとエラー

コメントがバイナリに影響を与えてしまう

cgo の // #include <stdio.h> や、embed の //go:embed hello.txt のこと。
こういうのコンパイラへの指示なんだからコメントとは別の文法を用意してある方が好き。

pragma みたいな予約語だと目立っていい気がするけど、謎の記号列でもいいと思う。とにかくコメント以外がいい。

Discussion

cielciel

condOpForXType

個人的に(皮肉混じりですが)IIfという関数名にして使ってます。。