AJazz AKS068 レビュー

生きとったんかいというくらい久しぶりのエントリ。お元気ですか?

今まで薄っぺらいモバイルキーボードで特に苦もなくやってきたけれども久しぶりにちゃんとしたキーボードを買った。 AJazzのキーボード、AKS068である。

最近はメカニカルキーボードの価格破壊が起こっていて検索するとかなり安いキーボードが表示されてビビる。ただし、やっぱりデザインが酷かったり、レイアウトが酷かったりで実際に買う気には以前はあまりなれなかった。

しかし最近AJazzさんの製品が比較的まともに見えてきて気になってきた。検索してみると中華キーボードのレビューをひたすら行われているYouTuberさんが見つかる。この方の評価が良かったので買ってみた次第。

最近はSWエンジニアさんの間で自作キーボードが流行っている模様で自分はあまり興味なく見ていた。自分の場合、キーボードの選択権など無いのが普通の環境だったから。しかし、最近はキーボード業界も随分と進んでいるようで、他のエンジニアの人々には笑われてしまうだろうが今回の件は自分にはとても新鮮な体験だった。

さてAKS068であるが、この製品は有線接続のみなのだが既に無線接続(Bluetooth、2.4GHz共)対応のProが海外では販売開始されている。

Ajazz AKS068 Proepomaker.jp

日本ではAjazzさんの製品はepomakerさんが扱っている模様。関係会社らしいけれども良く知らない。技適対応待ちなのか私が見た時点ではpreorderで販売前だった。

無印の有線版は既に販売ページが海外のAJazz本家でもAliExpressでもどこにも無く、ひょっとしたら今売っているものが無くなったら終了なのかもしれない。

AKS068の優れた点を並べてみると以下となる

  • アリスレイアウト
  • ガスケット対応
  • VIA対応

アリスレイアウトはAKS068はあまり左右に分離はしておらず普通のキーボードに近い。スペースキーが2つあることがかつての親指シフトキーボードを想起させて嬉しくなる。

ガスケットは正直良くわからないのでパス

ソフトウェアエンジニア的にとても面白いのがVIA対応だ。何とこのキーボード、キーのレイアウトが自由にほとんど制約無く全面的に変更が可能なのだ。しかもWebアプリを用いて。

VIAのアプリがここになる。

usevia.app

既にそんなに新しい話では無い模様で解説サイトも探すといくらでもあった。 比較的、良かったのがこちら。既に3年前かな。

salicylic-acid3.hatenablog.com

キーボードのレイアウトがWebアプリで自在に変更できるとかちょっと夢みたい。以前はDIPスイッチでちょっとだけ入れ替えが可能という話だったかに思うけれども技術はいつでも進んでいますね。感動です。

んで、これはどうやらQMKというOSSファームウェアで可能になった話らしい。ただ全く詳しい話は知らないのでこれから学習する次第

docs.qmk.fm

VIAは多くのVIA対応キーボードにデフォルトで対応しているようですが、VIAアプリが未対応なVIAプロトコル対応キーボードでもレイアウトをJSONファイルで指定すると使えるという話。AKS068はもちろん(?)未対応で利用にはJSONファイルが必須。

ところがこの製品の箱にもマニュアルにもURLが1つも無い(笑 AJazzで検索しても公式サイトが全く結果に出ないでAmazonとかの販売ページばかりが出る。 なので少しだけ公式サイトを探すのには苦労しました。 ここに貼っておきます。

www.a-jazz.com

ここの一番上からDLページに進んでAKS068を入力すると国際版のドライバがDLできます。 この中に目的のJSONが入っています。 既にPro版と無印版の2つのJSONが入っているので私は無印向けを利用しました。

レイアウトの変更は問題無くできたので割愛。 VIAは変更後の設定をexportできないようでその点だけ不満でした。 Fn+Spaceを3秒押しでリセットできるのですが、再変更が面倒なこと。どうするべきなのだろう?

日本では既にVIA相当のWebアプリ、Remapを超有名なエンジニアさん達が開発されているのですね。 この点で今の自作キーボードブームとやっと話が繋がるのでした(笑

salicylic-acid3.hatenablog.com

AKS068ですが、Remapで利用するとAkS068の利用するプロトコルが古いようで、Remapの0.18版を使うように誘導されますが、残念ながらJSONを使ってもRemap 0.18ではレイアウトの表示に失敗して使えませんでした。 元々、VIAのほうが面倒でRemapが開発された模様ですが、現在のVIAも進化しているようでAKS068のレイアウト変更にはVIAで特に問題もなくここで調べるのを止めています。

そんな訳で私はAKS068のESCキーをチルダに戻して、CapsLockを無くして、ESCやCtrl、Fnを打ち易い場所に整理して使ってます。60%キーボードとは思えないくらい楽になりました。

VIAという仕組み、素敵ですねという自分用メモでした。

Goの型アサーションと型変換

(更新) この記事に書かれていることは大体、以下のStackoverflowのベストアンサーに書かれていることから学ばせて頂いた。

go - X does not implement Y (... method has a pointer receiver) - Stack Overflow

毎日、少しづつGoでプログラムを書いているのだが、Goの融通がきかない点と記述の冗長性には少々疲れてきている。もちろん、これは誰が書いても同じになるという保守性、可読性からの観点による利点と、同時に暗黙の型変換によるプログラマが意図しないバグの作り込みを取り除くためという言語設計者の意図も良く理解できる。*1しかし、わかってはいても他言語に比べて生産性の下落は否めない気がする。コンパイラによる未然の問題解決は最近のトレンドであり、より安全なプログラムを求める流れの中で当然の帰結でもある。でも、できれば生産性を犠牲にせずともよしなにしてくれるほうへと発展してくれることを祈る。

Goの融通のきかなさが最も現われるのはやはり多様な数値型が存在する割に、その上で数値演算における暗黙の型変換が一切無い点だろう。

例えば一番極端な例としてはint、int32、int64の間でも、例え実際には同じ型であっても暗黙の型変換が行われない点だ。

https://play.golang.org/p/lnACZ_W_84H

var n int64 = 1
var m int32 = 2
Println(n + m) // invalid operation: n + m (mismatched types int64 and int32)

これはまだわかりやすく、修正し易い。しかしint64とint32の型整合ならint64に寄せるのが普通なのでこれが自動で解決されないのは悲しい。逆にint32に寄せるなら明示的なオーバーフロー対策が必要になるだろう。明示的なキャストの必要性だけでは問題解決に至らない気がする。*2

GoはCと同様にポインタが存在する。構造体のポインタでもメンバのアクセスに'->'を使う必要はない等、一定の暗黙での変換は行ってくれている。しかし代入時には面倒なことに暗黙の型変換は行ってくれないばかりか、明示的なキャストも制限が大きく、型アサーションとif判定と代入を長々と行わなければならない。これが結構苦痛だ。

ここで自分が引っ掛かったのがReaderだ。io.Readerはinterfaceで様々な型がこのio.Readerを実装している。例えば最も身近なものはos.Fileだろう。os.Openでファイルを開くとos.Fileが返ってくるが、os.Fileはio.Readerを実装している。

https://golang.org/pkg/os/#File.Read

ここでメソッド定義のレシーバーに注目。

func (f *File) Read(b []byte) (n int, err error)

Fileのio.Readの実装のレシーバはFileのポインタであるポインタ型だ。

レシーバの型は単一のdefined typeかそのpointerのどちらかであると仕様で決められている。

https://golang.org/ref/spec#Method_declarations

The receiver is specified via an extra parameter section preceding the method name. That parameter section must declare a single non-variadic parameter, the receiver. Its type must be a defined type T or a pointer to a defined type T.

公式訳が存在するのか知らないがdefined typeが定義型、または単に型、ポインタはポインタ型と呼ばれるのが一般的な模様だ。

この時、言語仕様ではdefined typeとpointer typeは明確に区別されており、ポインタ型のほうがより多くのmethodを持つことが可能である。ポインタ型は型のメソッドだけでなく、専用のメソッドを拡張し、両方を持つことが可能だ。

https://golang.org/ref/spec#Method_sets

The method set of any other type T consists of all methods declared with receiver type T. The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).

method set(メソッド集合)とは非常にユニークな概念に感じる。継承の無いGo言語では型に含まれるメソッドは継承で引き継ぐのではなく、他のインターフェイスや型を埋め込むことで得ることも可能だ。

https://golang.org/ref/spec#Struct_types

A field declared with a type but no explicit field name is called an embedded field. (snip) A field or method f of an embedded field in a struct x is called promoted if x.f is a legal selector that denotes that field or method f.

Promoted fields act like ordinary fields of a struct

恐らくこのためであろうが、型とポインタ型は厳しく区別される。そんなの当たり前だと思うかもしれない。C言語だってそうだったではないかと。しかし、メソッドのレシーバの型違いを理解することが実は重要だったという事実は恐らく初心者の内は気付かないものではないだろうか

さて以前にGoでもJavaと同様にioにはReaderとBufferedReaderが存在するということを先日書いた。Goではbufio.Readerになる。

https://golang.org/pkg/bufio/#NewReader

自分がはまったのはこのbufioの利用においてである。

あるfileを開いてio.Readerを得た後にbufferingを利用するためにbufio.Readerで包むと考えよう。Javaでも良くある話だ。

この時、bufio.NewReaderを用いる。

https://golang.org/pkg/bufio/#NewReader

func NewReader(rd io.Reader) *Reader

さて、ここで気をつけなければいけないのはos.Openで返ってきたFileはポインタ型、*Fileだ。引数io.Readerはインターフェイスであるためにio.Readerの定義するメソッド、Readを実装していれば何でも放り込める。ここで先程の定義を思い出して欲しいのだが、FileのReadメソッドのレシーバは*Fileを指定。つまりポインタ型だ。つまり、Fileのメソッド、Readはポインタ型のFileにしか存在しない。ここでは*Fileなのでそのまま渡すことが可能だ。*3

https://play.golang.org/p/CEtqZhSz7-A

func main() {
    f, _ := os.Open("/tmp")
    r := bufio.NewReader(f)
    println(r)
}

さて、無理矢理ポインタでないFileを食わせるとどうなるか。

https://play.golang.org/p/QtcOttzHJ4n

func main() {
    r := bufio.NewReader(os.File{})
    println(r)
}

実行時エラーでなくコンパイルエラーになる。

./prog.go:9:30: cannot use os.File literal (type os.File) as type io.Reader in argument to bufio.NewReader: os.File does not implement io.Reader (Read method has pointer receiver)

これをどれだけのGo初心者が理解しているだろうか。io.Readerはインターフェイスだ。だからポインタ表記は存在し無い。しかし、引数はこの場合、ポインタ型の*os.Fileでないとエラーになる。なぜならFileのメソッド、Readの実装はレシーバがポインタ型だから定義型のos.Fileにはメソッド、Readが存在しない。

さて、今は無理矢理エラーを出したからこれが問題になるのか実感できないだろう。実際にはまった例を出す。

パッケージmainにio.Readerを実装する型Readerを実装する。Readerはio.Readerを実装する型を引数に取る。ユーザは何かio.Readerの実装(例えばos.File)を準備し、Readerに与え、Readerが処理した結果を新たにReadする。これは例えばGoの標準ライブラリであるcompress.gzip等が全く同じことをしている。

https://golang.org/pkg/compress/gzip/

さて、ReaderもまたNewReaderメソッドを実装するとしよう。引数rはio.Readerを実装する。

func (m *Reader) NewReader(r io.Reader) *Reader

非常にややこしいが、goでは単純にReaderと書いた場合はそのソースコードが存在するpackageのReaderになるのでここではReaderはmain.Readerであることに注意

さて、Readerは大量のデータを扱うので引数rが必ずバッファリングされていること、bufio.Readerで包まれていることを確認したいとする。

以下のように実装した。

https://play.golang.org/p/E0m2bptNwL8

type Reader struct {
  r bufio.Reader
}

func NewReader(r io.Reader) Reader {
  newr := new(Reader)
  newr.r = (bufio.Reader)(r)
  return newr
}

func main() {
    f, _ := os.Open("/dev/stdin")
    r := NewReader(f)
    println(r)
}

これが間違いだらけであることに気付かれるだろうか?

実行すると当然コンパイルすら通らない

./prog.go:15:26: cannot convert r (type io.Reader) to type bufio.Reader

./prog.go:16:3: cannot use newr (type *Reader) as type Reader in return argument

15行は落ちて当然でio.Readerを実装している型が全てbufio.Readerに変換できるなんてことはない。それにio.Readerを実装しているのは*bufio.Readerであってbufio.Readerではない。

https://golang.org/src/bufio/bufio.go?s=4864:4914#L187

16行はちょっと変化球でnewrは14行でnewで作っているので実の値でなくポインタである。':='で何でも代入していると型が何なのか考えなくなるが、varで型を指定して代入すると落ちるので良くわかる。

16行をこうすると、

   return *newr

とするととりあえず16行はコンパイルが通るようになる。しかし、これで良いだろうか?今は説明のために型Readerはとても小さい。しかし、実際には型Readerは処理の実装を含め、structのfieldの数もそれなりに増えていく。大きな構造体を値渡しにするのはお勧めできない。実際、標準ライブラリのReader実装も皆、当たり前のようにポインタ渡しだ。だから直すなら、13行の宣言のほうをこう直すべきだ。

func NewReader(r io.Reader) *Reader {

さて、では15行のほうを直そう。 実はbufio.NewReaderには*bufio.Readerを渡すこともできる。引数がio.Readerの実装であることしか見ていないからだ。従って以下のように簡単に直しても良いように思われる。

https://play.golang.org/p/JdvMRbtbh-q

type Reader struct {
  r bufio.Reader
}

func NewReader(r io.Reader) *Reader {
  newr := new(Reader)
  newr.r = bufio.NewReader(r)
  return newr
}

実行するとわかるがこれも落ちる。

./prog.go:15:10: cannot use bufio.NewReader(r) (type *bufio.Reader) as type bufio.Reader in assignment

Reader.rは実型なのにbufio.NewReaderの返り値はポインタなので代入できないという訳だ。

では15行を以下のようにすればコンパイルは通る

https://play.golang.org/p/Vg2_hCtSdvR

  new.r = *bufio.NewReader(r)

でもちょっと待って。さっきと同じだがbufioのメソッドはレシーバがポインタ型である。なぜ実型で保存する必要があるのか。直すべきは10行だった。

https://play.golang.org/p/Vxucsz5F0OV

type Reader struct {
  r *bufio.Reader
}

さて、これで終了だろうか?

もし引数がbufio.Readerだったらbufioにbufioをかけるの無駄じゃね?と思うはずである。

実際、コンパイラを通すだけならこんなコードでも通るのだ。

https://play.golang.org/p/rsI7cuZhBIq

  newr.r = bufio.NewReader(bufio.NewReader(bufio.NewReader(bufio.NewReader(bufio.NewReader(r)))))

このような無駄を憎むなら型アサーションを行わねばならない。やっと本題に入った :-)

さて、本題に入ったのに少し戻るけど、先程Readerのfield、rの型を*bufio.Readerに修正した。この場合に一番、最初の明示的な型変換をかけるとこうなる。

https://play.golang.org/p/HlxTNaZ2mjz

  newr.r = (*bufio.Reader)(r)

エラーが少し変わる。

./prog.go:15:27: cannot convert r (type io.Reader) to type *bufio.Reader: need type assertion

*bufio.Readerはio.Readerを実装しているけれども、事前に型アサーションが必要だよと教えてくれる訳だ。

アサーションの仕様は以下。

https://golang.org/ref/spec#Type_assertions

x.(T)の形で利用するがこの時、xはインターフェイス型だと指定されている。

アサーションは単体で用いると他言語のassertと同じで型チェックのみを行い、異なるとpanicを発生する。

アサーションを代入文や初期化に利用すると第二の返り値でassertの成否が判定できる。

さて、まず15行を以下のように変えてみよう。

https://play.golang.org/p/MIb8AjBX9SD

  newr.r = r.(bufio.Reader)

コンパイラが不可能なアサーションだと教えてくれる。

./prog.go:15:13: impossible type assertion: bufio.Reader does not implement io.Reader (Read method has pointer receiver)

io.Readerを実装しているのはポインタレシーバだとまで教えてくれる。

で、以下のように変更する。

https://play.golang.org/p/auKCHLcklRk

  newr.r = r.(*bufio.Reader)

するとコンパイルは通るが実行するとpanicで落ちる。

panic: interface conversion: io.Reader is *os.File, not *bufio.Reader

引数rは*os.Fileであって*bufio.Readerではないという訳だ。 今の実装ではまさにその通りだ。

では第二返り値を利用して判定するのだが、その前にまた悪戯してみよう。今度は第二返り値を捨ててみよう。

https://play.golang.org/p/vjGLBcEtH9V

  newr.r, _ = r.(*bufio.Reader)

なんとコンパイルも通り、実行もできる。ただし、*bufio.ReaderであるReader.rの値は0x0とゼロ値だw これはアサーションが落ちているのだから当然である。Goでは_は返り値を捨てるためだけに用いり、_を変数として判定には使えないので新たに名前を付けて代入する必要がある。

では第二返り値をきちんと代入してみよう。以下ではどうだろう。

https://play.golang.org/p/MCZqJ5ZbGsR

  newr.r, ok := r.(*bufio.Reader)

./prog.go:15:7: non-name newr.r on left side of :=

これもエラーとなる。':='の代入はshort variable declarationとのことで、ここにはidentifierしか認められない。つまりnew.rがIDとして相応しくないと蹴られている。

https://golang.org/ref/spec#Short_variable_declarations

んなアホなという感じだ。このような記載は認めるべきかもしれないとの議論がissueで行われている。

github.com

さて、ここでGoの冗長さが発揮される。':='では駄目なので'='を使うという方針で修正するならokを事前に宣言しなければならない。

https://play.golang.org/p/VvzAULeyq3x

  var ok bool
  newr.r, ok = r.(*bufio.Reader)
  if !ok {
    newr.r = bufio.NewReader(r)
  }

これで全てがうまく行った。 別解

https://play.golang.org/p/ynpw7bk0d2l

  var ok bool
  if newr.r, ok = r.(*bufio.Reader); !ok {
    newr.r = bufio.NewReader(r)
  }

さて、さらに別解としてfieldへの代入が':='では駄目なのだから別の変数に一旦入れれば良いとの解法もある。

https://play.golang.org/p/PEwUSxxtTML

  if r2, ok := r.(*bufio.Reader); ok {
    newr.r = r2
  } else {
    newr.r = bufio.NewReader(r)
  }

あまりの冗長さに身悶えしてしまう。ちなみにこの場合にr2でなくrを代入しようとするとまたエラーで落ちる。変換結果はr2に入っているだけでrはアサーションが成功しても何も変わらないことに注意が必要。

以上、Goの型アサーションと型変換の冗長さについて、冗長に書いてみた。書いている間に色々な情報が出てきて自分でも納得のいく出来になったのが幸いだ。こんな長い記事を果たして投稿できるのかが気掛かり。

*1:実際、C/C++ではintとuintの区別をしないがためにセキュリティホールに発展する場合もある。Javaはuintを廃止した。しかし、Javaはそのためにバイト演算が非常に面倒になっている。

*2:Goではexplicit conversionと言いcastという言葉は一切使用されていない

*3:わかり易くするためerrを捨てているけど良い子は真似しないこと。

Goにはなぜ基本的なデータ構造が標準ライブラリにないのか?

だらだらとpackageのdocumentを読んでいる。楽しくて仕方が無いのだけれど、変なことに気付いた。Goには標準的なデータ構造のライブラリが無い。例えば基本的なSetすらない。色々なpkgでtreeは実装されているが、treeのライブラリは無い。

(BigNumberや複素数等はある。後、suffixarrayがあるくらいかな?)

これは面白い。Java以来、新規の言語の標準ライブラリにはそういった基本的なアルゴリズムやデータ構造が標準で用意されてきた。そういうものが無いのは何でなんだろう。

btreeはGoogle自身が作っていた。

https://github.com/google/btree

4、5年前の物だが最近でもパフォーマンス改善が行われている。しかし、標準ライブラリでなく、Googlegithubである。

何だか面白くて検索してみたらこんなのが。

data structures - golang why don't we have a set datastructure - Stack Overflow

ダメな質問としてcloseされているが、トップの回答ではGo言語にはGenericsが無いからだとdisられていた :-)

他のコメントにGoのspiritでは自前で実装するのが正しいからとある。証拠は無いが何か納得 :-)

先日のPythonにおけるstdlibはコードが死ぬ場所議論もあったのでこういう言語設計もあるかなぁとは思う。しかし、やっぱりGoは初心者向けではない感じ。自分で実装するか、まともな実装を探して使うか。後、外部コードをバリバリ使うのが正しいとの嗜好なのは今では他の言語でも普通だけど、日本のSI屋さんはGoは大丈夫なんだろうか? GAEとかだけなら十分に標準処理があるのかな。まぁ、お客さん次第か。

GoのPlaygroundでは乱数の種を撒けない

GoのPlaygroundでは時間や乱数が一意に決定され変更できない。

go - Golang random number generator how to seed properly - Stack Overflow

Inside the Go Playground - The Go Blog

コードに対するhashでmemcachedに結果を保存しており、人気のあるコードがGCPの課金を増やさないようチェックしているそう。

Playgroundはやっぱり楽でつい自前でコンパイルするよりそっちを使ってしまっていたのでこれは少々残念な制約。

んで、上のStackoverflowでは面白い指摘が出ている。Goでは時間がナノ秒まで言語仕様上は取得可能。しかし、実際の処理系がそこまで精度があるのは稀なので良くある現在時間をseedにするのは止めろよとのコメントが存在。より正しくは"crypto/rand"をseedに使えと推奨している。この前提が正しいのかちょっと自分のPCでtime.Nowを実行してみた所、自分の環境(WSL)ではtime.Nowの連続実行の差分がだいたい平均で2μsという所で1ms程は酷くなかった。しかし、自分の書いたコードがどこで実行されるのかはわからんので考慮の必要はあるのだろうか?

そもそも、暗号レベルの高い安全性がいるような状況でmath.randを使うことが正しいのかどうかという疑問はあるかもしれないが、ちょっと楽しい議論だった。

go concurrency

とりあえずgoのchannel周りは後回ししようと思っていたのだけれど、結局、勉強を始めてしまった。しかし、他言語よりは遥に簡単なGoでも難しい局面は多量に出てくる。

とりあえず、goroutineはどうやってscheduleしているのか?

goscheduler.md · GitHub

docs.google.com

素晴しい日本語解説が出てきたけど、とりあえず素人としてはgoroutineはシステムコール呼び出しで切り替えられると覚えておけば十分だろうと思う。(と言って逃げる :-)

もちろん、実行順は不定と考えるべしだし、同じgoroutineが継続される場合もあるだろう。

それよりchannel作る時の表記がchanなのがC言語っぽくてアレ。 また<-演算子は左結合なのでカッコが必要な場合も。言語仕様に載っている複雑な例を見ると頭が痛くなるのでお勧め。

んで、go blogからだけどRob Pike自身がGoのconcurrencyを説明している次のスライドが超お勧め

https://talks.golang.org/2012/waza.slide#1

非常に簡単なポンチ絵から始めてconcurrencyとparallelの違いを学び、concurrencyでも十分に速いし、そこからparallelへと発展できるんだよとの説明を行う。

その次に標準ライブラリのheapを用いてload balancerを実装してみせる。

この流れが非常にわかりやすい。

まぁ、結局、この資料ではロックについて触れていないのでsync pkgを使い始める辺りから地獄も待っているのでしょうけど。多分、goroutine大量、メッセージ通信、非共有メモリを堅実に守っていれば大抵の問題は最速でなくても優しく解けるのでは。

先は長いのだろうけど、自分のペースで頑張る。

なお、この資料でheapの存在を知り、上のtreeが無い問題にぶち当たる。またGo言語の継承が無いプログラムは本当に面白い。heapの各メソッドを自前で実装してるのに、heap.Initは突然標準の物を使う。従来のclassによるオブジェクト指向しか知らないとこの感覚になれるのが多分、大変。後、go funcで無名関数をガンガン実行していく部分はJSや関数型に慣れが必要でこれもちょっとアレだけど最近なら当たり前なんだろうか。closureとして使っている部分が多いのも慣れていない人には意味がわからない気がしてならない。*1

Goって結構大変な言語な気がする。それでも非常に人気が高いのはやっぱりGoogle先生の言語だからだろうか。もちろん、現場のクラウド化でCPU使用率の低いコンパイラ言語への移行が多いとか、バイナリ1つで配布できるとかの利点も理解しているのだけれど。Goもv2の話が出てきていて特に新しい言語と言える訳でもないけど、これくらい新しい言語で知的興奮が得られる仕事ができると良いなぁと思う。

Goでendianの判定

Goは実行系でCPUのarchが変わるのでデータが保存される順、endianが変わる。またintもJavaと違いサイズが固定されておらず、int64かint32になる。

このような点は大抵は問題にならないが必要になる場合ももちろん存在。判定するにはgo envで出る環境変数、GOARCHの値を見るのも1つの手段だ。では、それが無い場合には?

Go言語でプログラムでendianを判定する方法が紹介されていた。

https://groups.google.com/d/msg/golang-nuts/zmh64YkqOV8/mc3wZRbykiIJ

やっぱりunsafe.Pointerを利用する模様 :-)

なお、playgroundでも動いた。意外だ。

https://play.golang.org/p/jYY2r_WzCPq

ところで、Goでbinaryをバリバリ読み書きする場合に、bytesやencoding/binaryを利用することになると思う。後者のソースがとても面白い。(なお、GoDocの優れている点として関数等の名前をクリックすると直でソースコードに飛んでくれるのはとても嬉しい。使いまくっている)

https://golang.org/src/encoding/binary/binary.go?s=5180:5243#L151

Readのソースを見て欲しい。関数はInterface{}を用いて自由度を上げながらtype assertionによる型switchでとんでもなく長くなっている。

これについてはドキュメントのほうに

This package favors simplicity over efficiency. Clients that require high-performance serialization, especially for large data structures, should look at more advanced solutions such as the encoding/gob package or protocol buffers.

との説明が。うん、確かにパフォーマンスは出なさそうに見える :-)

面白いのはLittleEndianとBigEndianの両定数はByteOrderインターフェイスを別々に専用の実装をしている点だ。この部分は専用のコードが実行されるので型switchしておらず、速そう。ただし、処理系のarchと目的のarchとの組み合わせを考慮せずに常に変換を実行している。なのでプログラマは実行処理系とデータの保存形式の組み合わせが同じだと断定できる場合には使わないほうが良い? (その場合、使う訳が無いとも言える :-)

たまたまソースまで見てしまってつい悪い癖が出たのだけれど、気になる人は他にもいるよね?

ところで、このプログラムにはコンパイラに対するバイト境界の確認ヒントコードが入っている。ポインタをずらすことでセキュリティ上問題のある攻撃ができたりするらしいが、ポインタ演算の無いGoでpointerをずらすというのはunsafeでやるんだろうか。

main終了でgoroutineも終了

理解できていなかったがmainが終わればgoroutineは死ぬ。言語説明で無限ループなgoroutineが大量に利用されているけれども安心。

するとerrWriterでloopが終わらなくてもchanで通知してmainが抜ければ速攻で終了か。(まだ言うか

Goのchannelに関するMemo

資料のリンク https://talks.golang.org/2012/concurrency.slide#53

複数の並行の実装を紹介するスライド Go: code that grows with grace

Goでの並行によるエラストテネスの篩

https://play.golang.org/p/9U22NfrXeq

Haskellみたいな美しいコードだ。無限整数列を生成して素数で割っていく。channelが渡されていくのでそれまでの素数の倍数は取り除かれた数列が渡されていく。現時点での素数で割り切れない最初の数もまた素数。10個見つけた時点でmainが終了するので他のgoroutineも終了。goroutineがどんどん増えていくコードを考える頭がスゴイ。

*1:自分は入社したばかりの初心者な人達が書いたプログラムの保守の経験が多いので正直、この辺りをきちんと理解できるプログラマがどれ程いるのかは気になってしまう。

Goのパッケージ、ディレクトリ構造とエラー処理

ここ数日悩んでいるGoのパッケージ、ディレクトリ構造とエラー処理の話。

何気にGoDocにgo buildのドキュメントもあるんだなぁと気付いた。考えてみれば、Goのソースは全て公開されているのだった。goコマンドのソースを眺めてみるとちょっと面白かった。goコマンドのmainは以下になる。

https://golang.org/src/cmd/go/main.go?h=package+main

こうやって見るとビルドを含むgoのサブコマンドはCommand型で抽象化され、cmd.Runにて実行される。別プロセスになる訳でもなく、呼び出しているだけの模様。

go buildの本体はここにある。

https://golang.org/src/cmd/go/internal/work/build.go?h=base.Command

ディレクトリ構造がやはりgoのmainはディレクトリgoの直下にあり、その他は直下のinternalのさらに奥にある。これは他でも推奨されている構造だ。またやたらとディレクトリは多い。ディレクトリにgoファイルとtest.goファイルだけが1つづつあるというのが普通にある。これから考えてもgoはflatな構成をせず、package別に細かくディレクトリを細分し、下向きに掘り下げるのは意図的な設計な模様だ。

これまでの言語と異なり、こうやるものだと飲み込むべきだ。Javaが発表された当初も開発者の所属ドメインをpackage名の一部としてディレクトリとして掘り下げていくのは抵抗が大きかったが直ぐに慣れた。Goも直ぐに慣れるだろう。

海外のRailsから移った人が書かれたパッケージレイアウトのベストプラクティスでも似たようなことが書かれていた。プログラムのdomainとなる外部に非依存なデータ構造のみをトップに置き、外部依存が必要なものは依存別にpackageを作りディレクトリを掘って掘り下げていく。

Standard Package Layout – Ben Johnson – Medium

この方はpostgresqlやhttp等の依存する手段別にパッケージを分けている。その後、mockも別パッケージにし、mainで依存性の接続先を選ぶ(広義のdependency injection)ようにすることをお勧めしている。

このようなベストプラクティスはプロジェクトがでかくなり次第、必ず必要になるので良く読んでおくべきだろう。

エラー処理

GoBlogを古いほうから順に読んでいたらエラーの話もやはり多いのだが、Practical GoのerrWriterパターンをPike氏自身が書いていた。

Errors are values - The Go Blog

著名な日本のエンジニア、Jxckさんとの心温まるエピソードが良い :-)

errWriterパターンは無駄な実行が気になっていたのだけれどもそれについても一言書いてあっった。

There is one significant drawback to this approach, at least for some applications: there is no way to know how much of the processing completed before the error occurred. If that information is important, a more fine-grained approach is necessary. Often, though, an all-or-nothing check at the end is sufficient.

まぁ、大抵はこれで十分との話。

私は最初のerrorが出た後の空回りが気になっていたのだけど解決するとしたらどうするべきなのだろうか。実はこんな記事があった。

panicはともかくrecoverに使いどころはほとんどない - Qiita

この記事を書かれたUeyamaさんはGoogleでGoの開発にも参加されているスーパーエンジニアさんでこの方の書かれた記事は本当に深くて面白い記事が多い。

で、この記事はpanicに対するrecoverはほとんどの場合使う必要が無いとの主張だが、記事内ではGoの標準ライブラリ内でもどれだけ利用が少ないかという形で実際に利用されているソースが示されている。やはり深い実行スタックから一気に抜け出す場合が多い模様。

で、記事の趣旨に反してしまうのだけれども、errWriterパターンでこのpanic、recoverのペアで抜ける実装というのはどうだろうかと考えた。ワンチャンはあるかも。ただ微妙なのはpanicが出るのを忘れるとJavaのRuntimeExceptionを潰すのを忘れた時のように、実行時にエラー終了してしまう点だろう。

Goにon error goto文があれば良かったのにとか思った。ただGoではerrはあくまでvalueでしかないので実行系で区別するのが難しいか。*1

blockを越えられるgoto文、C言語のsetjmp/longjmp、Unixのsignal、いやいっそ例外に戻るか?悩みはつきない :-)

Goにはsignal handlerがあるんだなぁ。

https://golang.org/pkg/os/signal/

signalは基本panicに変換されるが、Notifyしておくとchannelに通知してくれる模様。それで気付いたのだけど、そもそもerrWriterをループするようなプログラムはgo funcにしておくと良いのか。で、errWriterがerrを見つけたらchannelで通知してmain threadは即終了と。errWriterを利用するgoroutineが終わらないけど。

errWriterのloop処理で、複数errWriterのwrite処理を呼ぶんだけれども、頭でやっぱりerrWriterにerrが出ているか一回くらいは見ればloopを抜けることができる。多分、loop数が大きいことが事前にわかっている場合にはそれくらいやるしかないのか。loop側でmainスレッドから終了通知をchanでもらっても同じ手間だし。

何か今一、コレだという納得感が無いまま終わる。 きちんとconcurrency patternsを覚えたら何か変わるだろうか?

goroutineは外から殺せないようだ。Javaも最初はThread.stop()があったのが何かの理由で無くなったのだっけか。何だっけ?

goroutineを外から殺せない理由はRussが次のように言っている。

https://groups.google.com/forum/#!msg/golang-nuts/SPAgm0TlWc0/SAbWyeBWMrEJ

"Killing individual goroutines is a very unstable thing to do: it's impossible to know what locks or other resources those goroutines had that still needed to be cleaned up for the program to continue running smoothly."

errWriterのloopをgoroutineにして、errが出たらpanicなりchannelで通知、mainスレッド側は殺せないのでチャンネルで通知なりするしかないと。以下のselectでchannelがcloseされると抜けるのが美しい。

How to kill a goroutine · YourBasic Go

ここまで書いてきたらどうもGoではこれらのためにcontextパッケージが用意されている模様。

https://golang.org/pkg/context/#example_WithCancel

ctxという物にはアレルギーがあるのだけれども標準ライブラリにあるのではしょうがない。郷に従えで飲み込むべきだ。

またerrWriterでは使えそうにないけど、そもそもチャンネルにはrangeが使用可能で、子のgoroutineでchannelをcloseすれば抜けるループが書ける。errWriterを回すループでchannelに進捗を報告し、errorが出たらクローズして抜けるようにするのもありか。

https://tour.golang.org/concurrency/4

まだまだGo力(ぢから)が足りない。精進せねば。

Goのerrorの柔軟性

何か文句ばかり付けているような文になってしまったが、Goのerrorがパッケージでそれぞれ拡張されていて目的に応じた柔軟なエラー対応が可能である点は利点として十分に意味がある。

Error handling and Go - The Go Blog

JSONやhttpでのエラー処理の説明。 ioやos.Openなんかでもfile.isExistなんかをエラーで処理できますね。 errorは文脈によっては出ても当然で対応可能な場合もあるし、そういう場合もerrorで判断が可能と。

*1:stringだけを持つstructなerrors.errorString型な場合が多いのだろうけど、この場合なら処理系はerrorを判別できるのだろうか。nil以外のerrorが返されたらerrorが発生したとして飛ばすon errorブロックとか作れるのだろうか。

Goでパッケージ名とコマンド名を同じにしたい場合に

微妙にGoのディレクト命名規則がイケていない気がしてならない。

先日も書いたがGoのディレクトリ名とパッケージ名の間には以下の規則が存在。

  1. パッケージ名とディレクトリ名は同じにする
  2. 実行ファイルを作成する場合のみ、例外としてパッケージ名はmainにしなければならない
  3. 同一ディレクトリ内のファイルに異なるパッケージ名の宣言は認められない

ここで実行ファイルの命名規則がさらに微妙で、

  • 実行ファイル名はディレクトリ名から取得される。ファイル名ではない

つまり、test.goにpackage mainとfunc main()がある場合、go buildの結果はtestではなくディレクトリ名になる。どうしてこうなった?

偉大なるC言語様は実行ファイル名がa.outになるというこれまた妙な仕様だった。しかし偉大なるmakeコマンドのdefaultの挙動がCのソースのファイル名から実行ファイルを作成した。つまり、test.cをmake testすれば実行ファイル、testが得られたのだ。

それに比べてGoが微妙なのはファイル名の意味が無いことだ。ディレクトリ名から取るならファイル名は何でも良いことになって意味が無い。

go buildには-oオプションが存在し、オブジェクト名を変更可能だ。これを用いてディレクトリ名と異なる実行ファイルをビルドすることも可能だ。しかし、go installには-oオプションが無いというこれまた微妙な仕様が過去に問題になりながら今でも修正されていない。go getでも困ってしまう。

で、現状、自分が体験している問題はあるデータ構造とそれに関わるpackageを同じ名前にしつつ、かつコマンド名も同じにしたい。かつ、全部、同じディレクトリに入れたい場合である*1

例えばgzipを実装すると考えよう。gzipをtypeにしてpublicに公開したい。自分が普通に考えると、ディレクトリ名とpackage名と型名はgzipになる。そしてコマンド名もgzipにしたい。このとき、ソースを全部1つのディレクトリに入れることは命名規則からできない。githubに公開することを前提にするならリポジトリ名=ディレクトリ名はgzipにしたいだろう。ディレクトリ名がgzipなら使えるパッケージ名はgzipに限定される*2。しかしpackage名が異なるソースは同じディレクトリに入れられない。だからコマンドとしての、package mainなgzipは別ディレクトリに入れねばならない。しかし型gzipを扱うコマンドgzipのソースを別リポジトリに入れるのは何か嫌だ。するとディレクトリを掘るしかない。しかしディレクトリ名が実行ファイルになるのでgzipリポジトリの直下にgzipという名のディレクトリを掘ることになる。何か嫌だ。

まぁ、実際、多くのプロジェクトは実行ファイル向けにcmdやpkgという名のディレクトリを掘り、さらにその中に実行ファイル名のディレクトリを掘るということをしている模様だ*3

CLI ツール開発を支える技術 2019春 P24

https://speakerdeck.com/izumin5210/techniques-that-support-building-cli-tools-2019-spring?slide=24

これ、実行ファイル名をディレクトリ名でなく、ファイル名から取れば済むことじゃなかったのだろうか?

ディレクトリを多段にするとmakeも複雑で面倒になる。気になってGo言語自身はどうやってbuildしているのか見てみたらbashスクリプトのみでやっているようだ。

https://golang.org/src/

1つのディレクトリでは1つしか実行ファイルを作れないという強烈な制限はgo buildが複数のパッケージを処理する場合に、ディレクトリ名を複数指定するという規約に繋っている。筋は通っている。go buildに複数のgoファイルを指定する場合には1つのパッケージに属すると見做される。

何かあまりにも変で自分の理解が間違っている可能性が高いのだけれども、何か不愉快なので愚痴を書いてみた。

蛇足

ここまで書いて実際のgzipは型がGzipでなくReader、Writerなのに気付いた。Practical Goか何かで型名は衝突しても全然問題がない、なぜならGoではpackage名の省略は行わないからだと書いてあったのでそんなものなのだろう。Goではpackage名と型名の組で名前なのだ。自分も「郷に入っては郷に従う」をしたいと思う。

型名があまり重要でなく、package名が、つまりはディレクトリ名が重要なのがGo言語なのだろう。対してファイル名はあまり重要でないのかもしれない。それはそれで筋は通っているのだが、小さなプログラムを簡単に書きたいという需要には向いていない気がする。まぁ、dockerとかk8sとかでかいシステムを作るのがGo言語なのだからそんなことは最初から考えられていないのかも。やっぱりGoは初心者向けでないと感じる。

後、Practical Goではmainはできるだけ小さくせよと書かれている。その効用もわかるのだけれど、上記の制約から1つのディレクトリにぽつんと1つmainファイルが小さく存在する姿は妙なものだと感じてならない。従うけどね。

*1:恐らくこれが間違いだけど、納得の行く説明はまだ見つかっていない。多分、どっかで読み飛ばしているに違いない

*2:shouldなのでimportに苦労しても良いなら変えても良い

*3:もしくはmainファイルをトップに置いて、他のファイルは全てそれより下に置くか。ただこの場合には型を公開する場合、import文が長くなる

Goに関するアレやコレや

Go CLI

先日のGoConの素晴しい発表に続いて、別に優れた発表資料が公開されていた。

CLI ツール開発を支える技術 2019春

CLI ツール開発を支える技術 2019春 / Techniques that support building CLI tools, 2019 Spring - Speaker Deck

この資料はまとめからさらに発展して筆者のお勧めを紹介して下さっている。 現状だとCobraとViperが良い模様。 最初にpflagから始めて拡張してくのが確実な道なようだ。

ただ、自分は結構、以下のkingpinを気に入ってしまった。

GitHub - alecthomas/kingpin: A Go (golang) command line and flag parser

まだどれを使うかはわからないし、上記のようなライブラリが必要な規模のプログラムも書いていないのだけれど、必要な時に、優れた資料2つに出会えた偶然をとても有り難く感じている。

Quizの件

先日のGoConのQuizの件、ちょっと調べていたのだが以下のような記載が見つかった。

Golang Packages - golangbot.com

A package can have multiple init functions (either in a single file or distributed across multiple files) and they are called in the order in which they are presented to the compiler

訳してみると

1つのpackageは複数のinit関数を持つことができる。(単一のファイル内でも、複数のファイルに分散されても)。複数のinit関数はコンパイラに現れた順に呼び出される。

この上の英文だがやたらとコピペされているがオフィシャルな文書では発見することができなかった。一応、現状には合っているのだが、できれば引き続き公式な定義を探したいとは思う。

Go is Google's language, not ours

Chris's Wiki :: blog/programming/GoIsGooglesLanguage

Goにgenericsがどうしても欲しい人がOpenGoでも作らないかとの嘆きを漏らしたのに対し、GoはGoogleの言語であり、コミュニティの物ではないとの記事。筆者はGoにおけるversioning、moduleの仕様策定に関し大きな心の傷を負った模様だ。

以前からGoogleOSSOSSではないとの議論は存在する。Androidが3でソースの公開を製品発表以後まで延期し「なんちゃってOSS」と評されたのはもうかなり過去の話だが、それ以外にもGoogleは常に決定権は自分達だけで所持し、コミュニティには口出しをさせなかった。この記事のコメント欄でもChromeのエンジンについての記載があるが、Chromeの変更では常にGoogleの一存による変更に対し大きな反対意見が起ころうとも無視されてきた。例えばabout:blankを廃止し新タブにGoogle検索を常に表示するように変更されたり*1chromeの頭に「あなた」との表示を始めたり*2、Chrome69ではゾンビクッキーを開始して大顰蹙を買ったりしている*3Googleという企業がGo言語の仕様をGoogle第一で考えるのはあり得る話だし、今後も気を付けなければならないのは事実だ。tensor flowがGoogleのサービスを第一に考慮しているのと同じだ。

JavaもかつてSunの言語に過ぎなかったのがあまりにも人気が出過ぎてコミュニティを作らざるを得なくなった経緯があった。その場合にもSunは商標を握り続け、また非商業に限定的なGPLとの二重ライセンスを維持。Sunを買収したOracleによりGoogleJava API著作権にて大火傷を負ったのは記憶に新しい。記事にて指摘されているがGo言語の商標はGoogleが握っている*4。Go言語自体のコードはBSDJavaよりはマシだ。しかしGoogleがAlphabetの集金組織としてよりマネタイズを重視した経営を続けている現状、いつ心変わりを起こしたりするかはわからない*5。いつでも依存を断ち切れるように不断の努力を行っておくことは重要だと考える。

標準ライブラリってどれほど必要なんだろうか?

Python Software Foundation News: Amber Brown: Batteries Included, But They're Leaking

Pythonの話なんだけれども面白い話だった。

According to Brown, “the standard library is where code sometimes goes to die,” because it is difficult and slow to contribute code there.

彼女が問題にしているのは標準ライブラリが大き過ぎる点、そして標準ライブラリにコードを追加することはイノベーションを阻害するという点だ。

確かにここ最近の開発はどの現場でも外部ライブラリを使うのが当たり前だ。ここ数日Go言語のCLI開発環境を調べるだけでも、標準のFlagsはあまり使い勝手が良くなく、外部のpackageを使うのが普通のようだ。恐らくSI屋で金融等の厳しい環境では今でも標準ライブラリを使うことしか認めない現場もあるのだろう。しかしJavaですらmaven経由でSpringを使うことがほぼ当たり前になった昨今、外部ライブラリを使わないことなどあるだろうか。外部ライブラリを受け入れない現場はイノベーションを否定し、開発効率を犠牲にしている。彼女の言う、標準ライブラリはコードが死ぬ場所だという主張は非常に的を射ているように感じる。

外部ライブラリを利用する場合にもバージョン管理やセキュリティ、ライセンス等、大きな問題が残る。Goでもやっとこさモジュール管理が搭載される。しかし今後も多分、外部ライブラリに依存する開発は続くと思われるので新規の言語には最小限の標準ライブラリと最初からの標準モジュール管理を期待したい。標準ライブラリはシステムコールのラッパー程度で良いのかも。

Goのパッケージとディレクトリ名

相変らず駆け出しのGo開発者で基本的なことで躓いているけれども、Goのディレクトリの命名規則

いきなり正解だが、

How to Write Go Code - The Go Programming Language

Go's convention is that the package name is the last element of the import path: the package imported as "crypto/rot13" should be named rot13.

Goではpackage名はインポートするpathの最後の部分だ。packageが"crypt/rot13"とimportされるなら、rot13と名前付けされるべきだ。

さてここで問題なのがこれshouldな点。そのため当然、違うpackage名を付けてもbuildできてしまう。しかし、importする時点で記載が面倒になるので素直に同じにすることが最善な模様。

例外が存在し、

Executable commands must always use package main.

実行命令ではpackageはmainにすることと釘を刺している。ここがより厳しいmust。

コンパイラがさらに厳しいのはpackage名の指定がディレクトリ内で常に一つである必要がある件だ。これ、package mainを作るなら別の独立したディレクトリを必ず作成しなければならないということ?go buildには-oオプションがあって出力名を変更できるのだけれども微妙に嫌な仕様だ。

パッケージ名の衝突に関しては気にするな、importする側で別名が付けられるとの記載

Effective Go - The Go Programming Language

import時での別名の利用法は言語仕様に

The Go Programming Language Specification - The Go Programming Language

"import ."でパッケージ名を省略可能になるがテスト以外の目的で使うなとの話。ハイ。

実践Goとエラー処理

これ書いとくの忘れた。

Practical Go: Real world advice for writing maintainable Go programs

とんでもなく良い記事ですが、長いので一気に読むのはかなりつらい。

最初のほうは割と当たり前?な内容なんですが、段々と実践的になってくる。特に関心したのはエラー処理のお話。

Practical Go: Real world advice for writing maintainable Go programs

最初のリンク先も面白いです。type assertionとか使ってます。

https://golang.org/ref/spec#Type_assertions

errorに対してmethodのinterfaceでduck typingを行うと。 上級者の匂いがしますね。

で、エラー処理を減らしたいならより良いユーティリティを使えという話。ReaderよりもScannerを使えとのお話。こういうの標準ライブラリのドキュメントにも書いておいて欲しいんですが。

実際、自分もpathをなめるのにdirectoryをOpenしてからReaddirnamesとか使っていたのですが、ioutilにReadDirとかあったりするんですね。こういうのの選択だけでエラー処理の記述が大分減るというのは厳しい話です。ドキュメント舐めるの重要。

さらにこの記事で紹介されるerrWriterは面白いですよね。ループ分関数呼出は行われるのでパフォーマンスとか気にしちゃいますが。

しかしGoのエラー処理は難しい。Goto文でまとめて蹴散らすのがベストなんだろうか?心理的抵抗があるけど。抽出しろ、設計が大事とのお話なんですが。

バッファリング

先日、ちょろっと書いたioのバッファリング、やっぱりbufioを通さないとダメなようですね。こ辺はJavaと同じか。

bufioのNewReaderの引数がReaderしかないので自前でやるしかない模様。ioutilのReadAllだとbufioは噛ませてないですが、binary.Bufferを噛ませていると。

src/io/ioutil/ioutil.go - The Go Programming Language

この辺りのお作法を覚えるのは大変ですね。自分もbinaryをいじる必要があるのですが、binary.bufferでなくbufioになるのは読み込みをstreamingで行う必要があるのと、書き込みがserialに書けないからなんですが。

バッファリングについて調べていて見つけた記事が面白かったんですが、

Go言語で競技プログラミングするときに使ってる入出力 - くじらにっき++

Goで競技プログラミングをされている方の記事。これを読むとやはり競技プログラミングのような大量なデータを処理する場合に、やはりScannerだけでは遅く、bufioを使う必要があると書かれていますね。

また出力でも標準出力はバッファリングされていないのでfmt.Printのみでやるとかなり遅くなってしまうと。やはりbufferingは大切ですね。

Goで競技プログラミングに関してはこんな面白い記事もありました。

競プロのテストを「go test」でやりたかったので作った - Qiita

やぁ、まだtestまで全然行けてないですわw

分割しようよ

はてダと違って1日に1投稿という制限が無いんですよね。もっと分割して投稿するべきなんですが、まだブログになれていないな :-)

*1:これは最近、about:blankが使えるように戻った模様だ

*2:これは削除された

*3:このせいでChromeを使うのを完全に止めた。今はVivaldiやBraveをFirefoxと併用している

*4:Go言語はGoogleの前から別の物が存在していたが

*5:実際、Androidでは多くの機能がサービス化され非公開となった

WSLでGoのdlvが動かないw

うーん、さんざんWSLを推しておいてアレだけどもWSLでdlvが動かない。 dlv version等は大丈夫だが、dlv debugとやるとpromptが返ってこない。 一応、中で動いてはいるようでCtrl-Cを押すと対象プログラムが動いてその出力だけが出る状態。

このせいか、Remote WSL経由でのVisual Studio Codeで開発はできるがデバッグが動かない。

これに関して調べてみると少し古いが以下のような記述が存在。(2017/05/01)

Ubuntu Bash Windows 10 dlv debug session hanging · Issue #810 · go-delve/delve · GitHub

Delve is supported on linux and windows not on the linux-windows chimera. I'm not even sure ptrace works in that environment. AFAIK even go isn't officially supported there yet (or possibly ever): golang/go#16628, golang/go#17365

意訳すると

DelveはLinuxWindowsはサポートしますがLinuxWindowsのキメラはサポートしません。ptraceが動くのかさえわかりません。私の知る限りではgoも公式にはまだ(恐らく今後も)サポートしていません。

と、かなりつれない記載。

リンク先を辿ると以下のページに。

Delve hangs and does not show a prompt on Ubuntu running on Windows using the Subsystem for Linux. · Issue #1235 · go-delve/delve · GitHub

Debuggers (gdb, lldb) do not work · Issue #875 · microsoft/WSL · GitHub

で、こちらに以下の記述。こちらは極、最近の話。(2019/05/03)

Only gdb works, lldb still needs some work

gdbだけ動くようになったそうな。

恐らく問題はキメラな点でしょう。6月の終わりまでにはWSL2が出るはず。WSL2は本物のLinuxカーネルなのできっと動くはずです。後、1月程、待ちましょう :-)

gdbでのデバッグ方法は公式にて詳細な説明有り。

Debugging Go Code with GDB - The Go Programming Language

初回起動時に以下のような警告が出たのですが、

warning: File "/usr/local/go1.12.5/src/runtime/runtime-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load". To enable execution of this file add add-auto-load-safe-path /usr/local/go1.12.5/src/runtime/runtime-gdb.py line to your configuration file "/home/user/.gdbinit".

セキュリティ上の制約の模様です。素直に従って$HOME/.gdbinitを作ると以降、警告は出なくなりました。

デバッグも問題なくできました。

単純に自分の使い方の問題の可能性もまだあるのですが、もし自分の環境では動いたという人がいたらぜひ教えて下さい。ここに書いても反応はなさそうですが :-)