Quantcast
Browsing Latest Articles All 25 Live
Mark channel Not-Safe-For-Work? (0 votes)
Are you the publisher? or about this channel.
No ratings yet.
Articles:

実装して理解するスライス #golang

はじめに

この記事はGoアドベントカレンダーの1日目の記事です。

スライスの実態

runtimeのコードをみるとGoのスライスは以下のように定義されています。

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

reflectパッケージのSliceHeaderを見ても次のような定義になっています。

type SliceHeader struct {
        Data uintptr
        Len  int
        Cap  int
}

つまり、Goのスライスは次の図のように、配列へのポインタと長さと容量を持った値として表現されています。

image.png

runtimeとreflectパッケージでポインタがunsafe.Pointeruintptrで表現方法は違いますが、どちらもポインタを表す値です。

unsafe.Pointerは任意型のポインタと相互変換可能な型です。一方で、uintptrは整数型でunsafe.Pointerに変換が可能な型です。uintptrは整数型の1つなので、int型などと同様に四則演算が可能です。

image.png

次のように、[]int型をreflect.SliceHeader型として解釈してみます。
まず、[]int型の変数であるnsのポインタを取り、unsafe.Pointer型に変換します。
変換した値はptrという変数に入れています。
次に、ptr*reflect.Slice型にキャストし、そのポインタが指す先の値をreflect.SliceHeader型の変数sに代入しています。

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func main() {
    ns := []int{10, 20, 30}
    // nsをunsafe.Pointerに変換する
    ptr := unsafe.Pointer(&ns)
    // ptrを*reflect.SliceHeaderにキャストして、それが指す値をsにいれる
    s := *(*reflect.SliceHeader)(ptr)
    fmt.Printf("%#v\n", s)
}

要素へのアクセス

reflect.SliceHeaderを用いてi番目のスライスの要素にアクセス方法を考えて見ましょう。reflect.SliceHeaderはスライスが参照している配列へのポインタを持っています。そのポインタは、スライスの0番目の要素を指すポインタです。

そのため、次のat関数のように、先頭へポインタから要素i個分のポインタを進めた場所がi番目の要素のポインタとなります。要素の型によって、1要素あたりどのくらいポインタを進めれば良いのかは違うため、次の例ではint型の場合に限定しています。任意の型のサイズを取得するには、unsafe.Sizeofを用います。

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func at(s reflect.SliceHeader, i int) unsafe.Pointer {
    // 先頭ポインタ + インデックス * int型のサイズ
    return unsafe.Pointer(s.Data + uintptr(i)*unsafe.Sizeof(int(0)))
}

func main() {
    a := [...]int{10, 20, 30}
    s := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(&a[0])), Len: 2, Cap: len(a)}
    *(*int)(at(s, 0)) = 100 // unsafe.Pointerを*intに変換して代入している
    fmt.Println(a)
}

要素の追加

ここでappend関数のような機能を実装してみたいと思います。
appendは追加する際に、スライスが参照している配列の容量が足りている場合と足りたない場合で挙動が異なるため、それぞれ考えてみます。

容量が足りる場合

容量が足りる場合は、単純に次の手順で要素の追加を行います。

  • 新しい要素をコピーする
  • 長さを更新する
package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func at(s reflect.SliceHeader, i int) unsafe.Pointer {
    // 先頭ポインタ + インデックス * int型のサイズ
    return unsafe.Pointer(s.Data + uintptr(i)*unsafe.Sizeof(int(0)))
}

func myAppend(s reflect.SliceHeader, vs ...int) reflect.SliceHeader {
    // 新しい要素の追加
    for i := 0; i < len(vs); i++ {
        *((*int)(at(s, s.Len+i))) = vs[i]
    }
    return reflect.SliceHeader{Data: s.Data, Len: s.Len + len(vs), Cap: s.Cap}
}

func main() {
    a := [...]int{10, 20, 30}
    // s := a[0:2] -> [10 20]
    s := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(&a[0])), Len: 2, Cap: len(a)}
    s = myAppend(s, 400)

    var ns []int
    *(*reflect.SliceHeader)(unsafe.Pointer(&ns)) = s
    fmt.Println(ns)
}

容量が足りない場合

一方で、容量が足りない場合は、次のように配列の再確保を行う必要があります。

  • 元のおよそ2倍の容量を確保しなおす
  • 配列へのポインタを貼り直す
  • 元の配列から要素をコピーする
  • 新しい要素をコピーする
  • 長さと容量を更新する

この手順でスライスのサイズを拡張するための関数growsliceを実装すると次のようになります。そして、myAppend内で容量が足りない場合にgrowsliceを呼んでやることでスライスを拡張してやることができます。

package main

import (
    "fmt"
    "reflect"
    "unsafe"
)

func at(s reflect.SliceHeader, i int) unsafe.Pointer {
    // 先頭ポインタ + インデックス * int型のサイズ
    return unsafe.Pointer(s.Data + uintptr(i)*unsafe.Sizeof(int(0)))
}

func myAppend(s reflect.SliceHeader, vs ...int) reflect.SliceHeader {
    // 容量が足りない場合
    if s.Len+len(vs) > s.Cap {
        s = growslice(s, s.Len+len(vs))
    }

    // 新しい要素の追加
    for i := 0; i < len(vs); i++ {
        *((*int)(at(s, s.Len+i))) = vs[i]
    }
    return reflect.SliceHeader{Data: s.Data, Len: s.Len + len(vs), Cap: s.Cap}
}

func growslice(old reflect.SliceHeader, cap int) reflect.SliceHeader {
    newcap := cap
    doublecap := old.Cap + old.Cap
    if cap < doublecap {
        newcap = doublecap
    }

    s := make([]int, old.Len, newcap)
    newslice := *(*reflect.SliceHeader)(unsafe.Pointer(&s))

    // 古いスライスから要素のコピー
    for i := 0; i < old.Len; i++ {
        *((*int)(at(newslice, i))) = *((*int)(at(old, i)))
    }

    return newslice
}

func main() {
    a := [...]int{10, 20, 30}
    // s := a[0:2] -> [10 20]
    s := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(&a[0])), Len: 2, Cap: len(a)}
    s = myAppend(s, 400, 500) // 溢れる

    var ns []int
    *(*reflect.SliceHeader)(unsafe.Pointer(&ns)) = s
    fmt.Println(ns)
}

ここで注意したいのは、myAppendの戻り値がreflect.SliceHeaderという点です。要素の追加が行われた場合、少なくとも長さは更新されるため、戻り値として返す必要があります。

これはreflect.SliceHeader型は構造体のため、引数で受け取った値のフィールドをいくら変更しても呼び出し元に影響がないからです。例えば、次の例を見てみるとs2.Lenを変更してもs1.Lenに影響がないことが分かります。

func main() {
    a := [...]int{10, 20, 30}
    s1 := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(&a[0])), Len: 2, Cap: len(a)}
    s2 := s1
    s2.Len = 3 // s1には影響ない

    fmt.Println(s1) // {4310908 2 3}
    fmt.Println(s2) // {4310908 3 3}
}

おわりに

この記事では、スライスがどのように表現されているかを調べ、スライスに関数する処理を実際に実装するとことで、スライスを理解することを試みました。

なお、ここで扱ったunsafe.Pointerは次の例のように扱い方を間違えると大変危険なので、ご注意ください。

func main() {
    v := struct {
        a [5]int
        b [2]int
    }{
        a: [...]int{10, 20, 30, 40, 50},
        b: [...]int{100, 200},
    }

    // capがlen(a)より大きい
    s := reflect.SliceHeader{Data: uintptr(unsafe.Pointer(&v.a[1])), Len: 2, Cap: 6}
    var ns []int
    *(*reflect.SliceHeader)(unsafe.Pointer(&ns)) = s

    // aを超えてappend
    ns = append(ns, 60, 70, 80)
    fmt.Println(ns, v.a, v.b)
}

GAE/Goにおけるコスト最適化 #golang

External article

UDP サーバーでクライアント毎に net.Conn を作る #golang

External article

Go言語の strings.Builder による文字列の連結の最適化とベンチマーク

この記事は、Go Advent Calendar 2018の4日目の記事です。実は5日目も投稿したのですが、カレンダーが空いてた&貯めてた記事あったのでここで投稿します!

3日目はkechakoさんの UDP サーバーでクライアント毎に net.Conn を作る #golang でした!

文字列結合が大量に発生すると、Go言語であろうとさすがにコストが高くなる。
そこで Go1.10 から実装された strings.Builder を試し、ベンチマークをとってみる。

LT;DL

strings.Builder を使った文字列結合は、普通に += 使うよりは断然早い!キャパシティの付加する strings.Builder.Grow() を使って前もってキャパシティを確保しておく方法が観測上は最速、使い心地も良い。

strings.Builder の中を確認する

strings.Builderは、Writeメソッドを使用して文字列等を効率的に構築するために使用されます。strings.Builder 自体はexportableなフィールドがない構造体です。

strings.Builder

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

内側に []byte が定義されています。

bufにappendしていく為の builder.WriteString()がある。中では単純にBuilder.bufに対して append が動いている。

strings.Builder.WriteString

func (b *Builder) WriteString(s string) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, s...)
    return len(s), nil
}

Builder.buf に append してきたものを最後にstring型にして返せば連結後の文字列が出来上がりという仕様。

strings.Builder.String

// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

さて一旦ここでベンチマークも含めて実際の使い方をみてみましょう。

package Builder_test

import (
    "strings"
    "testing"
)

// 愚直に += による実装
func joinWithPlus(strs ...string) string {
    var ret string
    for _, str := range strs {
        ret += str
    }
    return ret
}

// Builder.Builder による実装
func joinWithBuilder(strs ...string) string {
    var sb strings.Builder
    for _, str := range strs {
        sb.WriteString(str)
    }
    return sb.String()
}

// 愚直に += による実装 のベンチマーク
func BenchmarkPlus(b *testing.B) {
    strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = joinWithPlus(strs...)
    }
}

// Builder.Builder による実装 のベンチマーク
func BenchmarkBuilder(b *testing.B) {
    strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = joinWithBuilder(strs...)
    }
}

$ go test -bench . -benchmem

goos: darwin
goarch: amd64
pkg: go-playground/benchmark/Builder
BenchmarkPlus-12                 5000000           292 ns/op         176 B/op          8 allocs/op
BenchmarkBuilder-12             20000000           114 ns/op          56 B/op          3 allocs/op

一番上が += を使った連結。二番目はstrings.Builderを使った方法。
さすがに Builder.Bulder の方が早い!しかし、まだアロケーションが数回起こってる。

キャパシティ付き[]byteへのappendの方が早いでしょ!

strings.Builder.WriteString の実装をもう一度みてみましょう。

// in src/strings/builder.go

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

// ...

func (b *Builder) WriteString(s string) (int, error) {
    b.copyCheck()
    b.buf = append(b.buf, s...)
    return len(s), nil
}

実態は[]byteappend()している形なので、キャパシティ付き[]byte の方がアロケーション数も減り、確実にコストは低くできる!

早速 strings.Builder.buf の容量を設定したいが、strings.Builder.buf には直でアクセスできない。しかし大丈夫。キャパシティを付与する Grow メソッドが提供されている。

strings.Builder.Grow

func (b *Builder) Grow(n int) {}

引数に渡した数だけbufの容量を確保する。これでキャパシティ付き[]byteが設定できる。早速ベンチマークをとってみる。

// Growを使って capsを確保した文字列結合
func joinWithBuilderAndGrow(strs ...string) string {
    var sb strings.Builder
    sb.Grow(30)
    for _, str := range strs {
        sb.WriteString(str)
    }
    return sb.String()
}

func BenchmarkBuilderAndGrow(b *testing.B) {
    strs := []string{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "iii"}

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _ = joinWithBuilderAndGrow(strs...)
    }
}
$ go test -bench . -benchmem

goos: darwin
goarch: amd64
pkg: go-playground/benchmark/Builder
BenchmarkPlus-12                 5000000           289 ns/op         176 B/op          8 allocs/op
BenchmarkBuilder-12             20000000           119 ns/op          56 B/op          3 allocs/op
BenchmarkBuilderAndGrow-12      20000000            64.7 ns/op        32 B/op          1 allocs/op

一番上が += を使った連結。二番目はstrings.Builderを使っているがGrowメソッドを使っていない。三番目はGrowも使っている。結果としてはアロケーションも減り、当然パフォーマンスも上がっている!!にくいね!!strings.Builder!!

strings.Builder についてちょっと便利なメソッド

1回初期化した strings.Builder は使いまわせる。特定の文字列を作成したら、ビルダーをリセットして新しい文字列を作成することもできます。

func joinedAndReverse(strs ...string) (string, string) {
    var sb strings.Builder
    for _, str := range strs {
        sb.WriteString(str)
    }
    joined := sb.String()

    // Reset呼んで strings.Builder.buf をnil にする
    sb.Reset()
    for i := len(strs) - 1; i >= 0; i-- {
        sb.WriteString(strs[i])
    }
    return joined, sb.String()
}

まとめ

strings.Builder便利!

ちょっと古いが、いろんな文字列結合を試した下記の記事も面白いので是非。下の記事で紹介されている最速の方法(キャパシティ指定付き[]byte)と Grow を使った方法はほぼ同等の結果だった(実装がほぼ同じだしね)。
https://qiita.com/ono_matope/items/d5e70d8a9ff2b54d5c37

追記

投稿してから気づいたんだけど、過去のtenntennさんの記事とモロ被りしてた。こっちの方がbytes.Bufferと比較してたりと詳しい。こちらもどうぞ!!
Go1.10で入るstrings.Builderを検証した #golang

Go言語の golang/go パッケージで初めての構文解析

この記事は、Go Advent Calendar 2018の5日目の記事です。

「Go言語でつくるインタプリタ」を読んで、プログラミング言語の「仕組み」に興味がでてきた。そして、Go言語だと構文解析が簡単に出来るとの噂が!ということで golang/go パッケージを触ってみると、Go言語で出来る事のイメージが更に広がった。せっかくなので自分のような初心者向けにハンズオン形式で紹介していきます。最終的にGo言語のソースコードから抽象構文木を所得して、そこから更に、抽象構文木を書き換えてGo言語のソースコードに変換するところまでやります。

とりあえず抽象構文木を手に入れてみる

抽象構文木 (abstract syntax tree、AST) とは言語の意味に関係ない情報を取り除き、意味に関係ある情報のみを取り出した(抽象した)木構造のデータ構造です。よってGo言語においてはスペース、括弧、改行文字などが省かれた木構造のデータ構造になる。これは見てみないと分からないと思うので、まずは抽象構文木を所得する術を確認し、早速、抽象構文木を確認しましょう。まずは下記を実行してみてください。

package main

import (
    "go/ast"
    "go/parser"
)

func main() {
    // ASTを所得
    expr, _ := parser.ParseExpr("A + 1")

    // AST をフォーマットして出力
    ast.Print(nil, expr)
}

これを実行すると、速攻で抽象構文木が手に入る。

$ go run main.go

 0  *ast.BinaryExpr {
 1  .  X: *ast.Ident {
 2  .  .  NamePos: 1
 3  .  .  Name: "A"
 4  .  .  Obj: *ast.Object {
 5  .  .  .  Kind: bad
 6  .  .  .  Name: ""
 7  .  .  }
 8  .  }
 9  .  OpPos: 3
10  .  Op: +
11  .  Y: *ast.BasicLit {
12  .  .  ValuePos: 5
13  .  .  Kind: INT
14  .  .  Value: "1"
15  .  }
16  }

ここでは2つのパッケージが使われている。

go/parser

go/parser はGo言語の構文解析を行う為のパッケージです。名前の通りGo言語用のParser(テキストをプログラムで扱えるようなデータ構造に変換する)を実装しています。出力はGo言語の抽象構文木(AST)になります。今回使ったメソッドは下記になります。

go/parser.ParseExpr

func ParseExpr(x string) (ast.Expr, error)

go/parser ドキュメントはこちら
https://godoc.org/go/parser

go/ast

go/ast はGo言語の抽象構文木 (AST) を表現する為の型が定義されているパッケージです。

先ほど見た go/parser.ParserExpr はGo言語の式(Expression) を構文解析し、式の抽象構文木を表現する ast.Expr インターフェースを返しています。ast.Expr の構造は下のようになります。

go/ast.Expr

type Expr interface {
    Node
    exprNode()
}

main関数で使った ast.Print は抽象構文木を読みやすい形で出力してくれます。

go/ast のドキュメントはこちら
https://godoc.org/go/ast

Go言語のファイルから抽象構文木を手にいれる

上で実装したように毎回string型でコードを渡すのはダルいので、Go言語で書かれたファイルの入力からASTを所得しましょう。

まずは構文解析対象となる example/example.go を作りましょう。

package example

import "log"

func add(n, m int) {
    log.Println(n + m)
}

ではこのファイルを入力として構文木を出してみましょう。
自分は下記のディレクトリ構造をとりますが皆さんお好みで!

.
├── example
│   └── example.go
└── main.go

まずは構文解析対象となる example.go を実装します。

package main

import (
    "fmt"
    "go/ast"
    "go/parser"
    "go/token"
)

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    for _, d := range f.Decls {
        ast.Print(fset, d)
    }
}

早速実行してみましょう!example.go の抽象構文木が所得できます。(結果が長いので省略します。実行して確認してみてください)

token.NewFileSet()は構文解析によって得られたASTのノードの詳細な位置情報(ファイル名と行番号、カラム位置など)を保持する token.FileSet 構造体のポインタを返しています。なぜこれを生成する必要があるのかは後々説明します。

parser.ParseFile の実装の詳細は省略しますが(ドキュメントは https://godoc.org/go/parser#ParseFile )、src が nil であるときは filename に指定されたファイルパスの内容を読み込みます。返ってくるのは ast.File 構造体です。

go/ast.File

type File struct {
    Doc        *CommentGroup   // associated documentation; or nil
    Package    token.Pos       // position of "package" keyword
    Name       *Ident          // package name
    Decls      []Decl          // top-level declarations; or nil
    Scope      *Scope          // package scope (this file only)
    Imports    []*ImportSpec   // imports in this file
    Unresolved []*Ident        // unresolved identifiers in this file
    Comments   []*CommentGroup // list of all comments in the source file
}

その中で先ほど実装に使った ast.Decls (Declarations の略)はトップレベルで宣言されたノードが返ってきます。

他のフィールドに目を当てると、例えば ast.File.Name はパッケージ名を格納し、ast.File.Imports はこのファイルで読み込んでいるパッケージのノードを表現します。

例えば下記では、パッケージのノードを所得して出力しています。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    for _, d := range f.Imports {
        ast.Print(fset, d)
    }
}

これを実行してみます。

$ go run main.go

 0  *ast.ImportSpec {
 1  .  Path: *ast.BasicLit {
 2  .  .  ValuePos: ./example/example.go:5:2
 3  .  .  Kind: STRING
 4  .  .  Value: "\"log\""
 5  .  }
 6  .  EndPos: -
 7  }

このように 構文解析することでGo言語のソースコードから必要な情報を所得しています。

抽象構文木(AST)の中をのぞいてみる

先ほど見た ast.File 構造体のなかにある ast.Decl の存在に思いを馳せることから始めましょう!
みてみると Node インターフェースが実装されています。

go/ast.Decl

type Decl interface {
    Node
    declNode()
}

実は抽象構文木のノードに対応する構造体は、全てこちらの ast.Node インタフェースを実装しています。

それでは、そもそもの Node インターフェースの中身はどうなっているのでしょうか。

go/ast.Node

type Node interface {
    Pos() token.Pos // position of first character belonging to the node
    End() token.Pos // position of first character immediately after the node
}

こちらも インターフェース型です。そのノードのソースコード上での位置を表現します。Node.Pos()Node.End()については後にみていきます。今はソースコード上の位置を返してくれるんだなくらいに覚えておいてください。

Decl ノードが ast.Node インターフェース を実装しているのを確認しましたが、他にも ast.Node インターフェースを実装しているものがあります。ここでは前に紹介したノードも含めて3つの主なサブインターフェースを紹介します。

go/ast.Decl

type Decl interface {
    Node
    declNode()
}

宣言に関するノード(declaration)。import や type や func がここに大別される。先ほど触れたast.File にも実装されています。

go/ast.Expr

type Expr interface {
    Node
    exprNode()
}

式に関するノード(expression) 識別子や演算、型など。この記事の初めに parser.ParseExpr でGo言語の式(A + 1)をstring型で渡してexpressionノードに変換する例を紹介しました。

go/ast.Stmt

文に関するノード(statement) if や for、switch など

type Stmt interface {
    Node
    stmtNode()
}

これ以外にもファイルやコメントなど、これらに分類されない構文ノードも存在します。
参考 Nodeの構成

ここまで紹介すれば、あとはGo言語のNodeの構造を参考にすれば、Go言語のソースコードから好きなものを取り出すことができます。

抽象構文木(AST)のトラバース

さて、抽象構文木が所得できたら、木構造やグラフの全てのノードを辿り(トラバース)し、再帰的に処理したくなってきます。なぜならフィールド名等を一個づつ指定して目的のノードにアクセスするのは type assertion や type switch が多発する為、非常にめんどくさいです。しかし、ご安心を。astパッケージには抽象構文木をトラバースする便利な関数が提供されています。

まずは使い方をみてみましょう。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    ast.Inspect(f, func(n ast.Node) bool {
        if v, ok := n.(*ast.FuncDecl); ok {
            fmt.Println(v.Name)
        }
        return true
    })
}

上はソースコードから関数名だけを引っこ抜いてくる処理です。実行してみてください。add関数の名前だけが所得できています。

$ go run main.go
add

ast.Inspect は、ASTを深さ優先でトラバースする関数です。ASTの任意のNodeを渡せばトラバースできます。そして、ast.FuncDeclast.Declインターフェースを実装しており、関数の宣言に関するノードを担当しています。参考 Nodeの構成

ソースコードの位置を所得する

さて、静的解析ではファイル名や行番号などを返したい場合があります。位置の所得についてみていきます。そういえば全ての Node には位置情報を返すメソッドが定義されていました。

type Node interface {
    Pos() token.Pos // position of first character belonging to the node
    End() token.Pos // position of first character immediately after the node
}

これを先ほどのコードに入れてファイル上の位置が所得できるか試してみましょう。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    ast.Inspect(f, func(n ast.Node) bool {
        if v, ok := n.(*ast.FuncDecl); ok {
            fmt.Println(v.Name)
            fmt.Println(v.Pos())
        }
        return true
    })
}

そして実行します。

$ go run main.go
add
32

32!?なんの数字??となります。ast.Node.Pos() は実はノードに属する最初の文字の位置からのbyte数を返してしまします。

ではどうやって行番号やカラム位置などに変換するのでしょうか。ここで go/token パッケージに注目する時がきました。もう一度先ほどのソースコードをみてみましょう。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    ast.Inspect(f, func(n ast.Node) bool {
        if v, ok := n.(*ast.FuncDecl); ok {
            fmt.Println(v.Name)
            fmt.Println(v.Pos())
        }
        return true
    })
}

実は今まで使っていた token.NewFileSet() は構文解析によって得られたASTのノードの詳細な位置情報(ファイル名と行番号、カラム位置など)を保持する token.FileSet 構造体のポインタを返しています。

これを使ってノードの詳細な位置を復元できます。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    ast.Inspect(f, func(n ast.Node) bool {
        if v, ok := n.(*ast.FuncDecl); ok {
            fmt.Println(v.Name)
            // v.Pos() から 詳細な位置情報(ファイル名と行番号、カラム位置)を復元
            fmt.Println(fset.Position(v.Pos()))
        }
        return true
    })
}

これを実行してみましょう。

go run main.go
add
./example/example.go:5:1

さっきの 32 という整数から 詳細な位置情報が復元できました。token.FileSet は抽象構文木のノードの位置情報を保持するので、これの情報を使って、token.FileSet から生えている Positionメソッドで、Pos値を詳細な位置情報Position値(ファイル名と行番号、カラム位置)に変換しています。上の例のように1度生成したtoken.FileSetは他で使いまわすのでどっかで保持しておくと良いですね。

go/token.FileSet.Positon

func (s *FileSet) Position(p Pos) (pos Position)

コードから得たASTを書き換えてファイルに出力する

さて、ここまででASTの構造をのぞいてきました。ここから独自のツールを作るとなると、ASTを書き換えてソースコードに出力するという流れが出てくる(例えば、関数名を書き換えたい、Field名を書き換えたい、コードをASTから自動生成したい等)。そのために、ASTを実際に書き換えて、それをGo言語のコードに変換して出力する流れをみてみましょう。まずはexample/example.goの関数名"add"を"plus"に書き換えてみます。

func main() {
    fset := token.NewFileSet()
    f, _ := parser.ParseFile(fset, "./example/example.go", nil, parser.Mode(0))

    ast.Inspect(f, func(n ast.Node) bool {
        if v, ok := n.(*ast.FuncDecl); ok {
            // ノードを直で書き換える
            v.Name = &ast.Ident{
                Name: "plus",
            }
        }
        return true
    })

    // 指定したFileがあったら開く、なかったら作る。
    file, err := os.OpenFile("example/result.go", os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()

    // go/printer パッケージの機能でASTからソースコードを作る。
    pp := &printer.Config{Tabwidth: 8, Mode: printer.UseSpaces | printer.TabIndent}
    pp.Fprint(file, fset, f)
}

今回はast.Inspectで探索して得たノードをそのまま書き換えています。(実は golang.org/x/tools/go/ast/astutil パッケージにはAST書き換えの便利な機能が提供されていますが、今回は使わずに書き換えてみます。)

そして今回は go/printer パッケージを使っています。こちらは AST からコードを生成する便利な関数が提供されている。今回使ったのは二つ。

go/printer.Config

type Config struct {
    Mode     Mode // default: 0
    Tabwidth int  // default: 8
    Indent   int  // default: 0 (all code is indented at least by this much)
}

こちらでprinterの出力時の設定が出来る。そして実際の出力は下のメソッドでイケる。

go/printer.Fprint

func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node interface{}) error

output に io.Writer を実装しているもの (今回はos.Fileを渡している) を渡す。fset は最初に作った token.FileSetを渡している。そして、node には実際に書き換えたASTを渡している。

これを実行するとexample/example.goの関数名"add"を"plus"に書き換えて、新しいファイルexample.result.goを作成します。

package example

import "log"

func plus(n, m int) {
    log.Println(n + m)
}

これで AST を書き換えて、ASTからGo言語のソースコードを出力する一連の流れができました!

まとめ

golang/go には今回使ったパッケージ以外にも 様々なパッケージやインターフェース、構造体があります。興味持った方で、もっと詳しく知りたいという方は下記の記事が参考になるでしょう。

goパッケージで簡単に静的解析して世界を広げよう #golang
静的解析を学ぶ際にどのような時にどのような記事を読めば良いのかをまとめてくれています!

https://qiita.com/tenntenn/items/beea3bd019ba92b4d62a
こちらは AST の解析時に型をチェックしたりする方法を紹介している。今回のハンズオンでは紹介してないですが、go/types は構文解析では結構使います。

typesパッケージ使った構文解析も余力あれば書きたい。

参考 Nodeの構成

Node
  Decl
    *BadDecl
    *FuncDecl
    *GenDecl
  Expr
    *ArrayType
    *BadExpr
    *BasicLit
    *BinaryExpr
    *CallExpr
    *ChanType
    *CompositeLit
    *Ellipsis
    *FuncLit
    *FuncType
    *Ident
    *IndexExpr
    *InterfaceType
    *KeyValueExpr
    *MapType
    *ParenExpr
    *SelectorExpr
    *SliceExpr
    *StarExpr
    *StructType
    *TypeAssertExpr
    *UnaryExpr
  Spec
    *ImportSpec
    *TypeSpec
    *ValueSpec
  Stmt
    *AssignStmt
    *BadStmt
    *BlockStmt
    *BranchStmt
    *CaseClause
    *CommClause
    *DeclStmt
    *DeferStmt
    *EmptyStmt
    *ExprStmt
    *ForStmt
    *GoStmt
    *IfStmt
    *IncDecStmt
    *LabeledStmt
    *RangeStmt
    *ReturnStmt
    *SelectStmt
    *SendStmt
    *SwitchStmt
    *TypeSwitchStmt
  *Comment
  *CommentGroup
  *Field
  *FieldList
  *File
  *Package

go runの実行をwrapしてhttp/httpsのrequestを手軽にtraceしたい

はじめに

この記事はGoアドベントカレンダー
の5日目の記事です。

自己紹介

ちょっとだけ自己紹介を。好きな標準ライブラリはgo/astやgo/typesです。愛憎半ば的なライブラリはx/tools/go/loaderです。今年はgomvpkgのlight版を作ったりしてました。

ちょっとした導入

溜まっていく書き捨てのコードたち

goで書くことに慣れてくるとけっこう何でもgoで書きたくなるときがあります。通常のgoの用途ということであればシングルバイナリということで何某かのツールを作ってビルドと言うことが多いですが、goが手に馴染んでくると共にちょっとした処理もgoで書いてしまって、スクリプト感覚で go run を呼び出すというようなことをしたくなります。

$ go run daily/scripts/xxxx/main.go -c <config>

時間の経過と共にそのような書き捨てのコードが徐々に溜まっていきます。

...と、言う風に書かれたコードたちありました。

初めて触ったコードでエラー

新しい人が途中からプロジェクトに参加しました(今までコードを書き溜めてきた人と違う人が操作していると思ってください)。

その人は、内部のコードについては不案内で、どのように書かれているかは把握していません。しかしとりあえずは存在するドキュメントを見ながら書かれた通りの引数を渡してこのスクリプトのようなものを実行しているようです。そのような書き捨ての便利スクリプトのうちの1つの動作が特定のURLにリクエストするようなものだったようでした。

そのコードがある日失敗します。

$ go run daily/scripts/xxxx/main.go -c <config>
failed (status 400). // panicですらないのでstack traceもなし

なにやら実行時にエラーを返しているようですが原因がわかりません。どこかにrequestをしていて400ということだけが分かるようです(よりひどいのはcontext canceledとだけ書かれたメッセージでしょうか?)。

丁寧なコードであればエラーメッセージから対処の仕方が分かるはずだけれど

丁寧にログが出力されているようなコード、丁寧にエラーハンドリングされているようなコードであれば、エラーメッセージを見れば対処の仕方が分かるものの、えてしてエラーを返しやすいコードというのはエラーハンドリングが雑であることが多かったりします(なにぶん出自は書き捨てのコードから始まっていました)。

例えば外部との通信が常に成功するという前提に立ったようなコードなどstatusが200である以外の処理が雑であったりします(はじめから丁寧なコードをというお叱りは真摯に受け止め善処したいというような声)。

今回のエラーに関して言えば、何やらサードパーティのライブラリを使って通信しているようですが、どのようなAPIにどのようなrequestを投げているかわかりません。まじめに中のコードやライブラリの実装を追えば分かるのかもしれませんが、あまり深入りしたくはありません。

このような時に、requestをtraceできると便利です(長い導入のおしまい)。

requestのtrace

requestのtraceと言っているのは以下の様なイメージのものです(実行時の通信をキャプチャしてその時のrequest,responseを覗き見したいということ)。

$ daily/scripts/xxxx/main.go <何か特殊なオプション> -c <config>

# 何やらrequestされたURLが分かる
request https://examples/xxx/yyy?v=1
request https://examples/xxx/zzz?v=2
request https://examples/xxx/xxx?v=1

$ ls <どこかのディレクトリ>
0000:https://examples/xxx/yyy?v=1
0001:https://examples/xxx/zzz?v=2
0002:https://examples/xxx/xxx?v=1

$ cat <どこかのディレクトリ>/0000:https://examples/xxx/yyy?v=1

URL <URLの表示>
<request header>
<request body>
<status>
<response header>
<respnose body>

とりあえずは、net/httpのDefaultClientのTransport(RoundTripper)をいじってあげると望みの機能をもたせることができそうです。詳しくは調べて見てほしいのですが、requestの前後で処理を間に挟むというようなことができます。

この方針は、対応が部分的になる可能性があるのですが、HTTPSの通信に対してもMITMを仕掛ける必要がなかったり、http.DefaultClientを尊重しているライブラリであればミドルウェアとの通信内容も見れたりで便利です(一例としては、Elasticsearchのolivere/elastic)。そしてgoのエコシステムとしては意外と治安が良いのか(?)それなりに多くのライブラリがhttp.DefaultClientを尊重しているようです。

trace機能を追加したい(httptrace)

手前味噌ではあるのですが、昔にgo-traceableというパッケージを作っていたことがありました。このパッケージは完成品ではないので実用するのはオススメしないのですが、このパッケージのhttptrace.Patch()を実行すると通信内容をnet/http/httputilの関数を使ってdumpしてくれます。

hello world

簡単な利用例を示します(readmeから持ってきました)。https://examples.net にアクセスしているだけのコードです。

package main

import (
    "fmt"
    "net/http"
    "os"

    "github.com/podhmo/go-traceable/httptrace"
)

func main() {
    // 事前に実行
    teardown := httptrace.Patch()
    defer teardown()

    // ここのrequestがtraceされる
    resp, err := http.Get("https://example.com")
    if err != nil {
        panic(err)
    }
    fmt.Println(resp.Status)
}

実行時にTRACEという環境変数を渡してあげると、requestをtraceします。

$ TRACE=1 go run main.go
GET / HTTP/1.1
Host: example.com
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

HTTP/2.0 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=604800
Content-Type: text/html
Date: Tue, 29 May 2018 22:26:06 GMT
Etag: "1541025663"
Expires: Tue, 05 Jun 2018 22:26:06 GMT
Last-Modified: Fri, 09 Aug 2013 23:54:35 GMT
Server: ECS (sjc/4E8D)
Vary: Accept-Encoding
X-Cache: HIT

<!doctype html>
<html>
...

<body>
<div>
    <h1>Example Domain</h1>
    <p>This domain is established to be used for illustrative examples in documents. You may use this
    domain in examples without prior coordination or asking for permission.</p>
    <p><a href="http://www.iana.org/domains/example">More information...</a></p>
</div>
</body>
</html>
200 OK

正確にいうと、TRACE=xxxxという環境変数について以下の様な振る舞いをします。

  • xxxxが存在しない場合、標準エラーにtrace内容を出力
  • xxxxが存在する場合、xxxxディレクトリにtrace内容を出力

HTTPSの通信に対してもMITM用のproxyを立ててそのproxy越しに通信などせずとも済むので便利です。便利なので2回言いました。

advanced (google api)

また、http.DefaultClientを尊重しているライブラリであれば、ライブラリを通したrequestに関してもtraceが効きます。そのようなライブラリは幾つかあるのですが、そのうちの1つはgoogle apiのライブラリです。

例えば、以下はgoogleのspreadsheetのAPIのquickstartの例をtraceしたときの例です。

mkdir -p output
TRACE=output run main.go
2018/12/04 01:51:50 trace to 0001https:@@sheets.googleapis.com@v4@spreadsheets@1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms@values@Class%20Data%21A2%3AE?alt=json&prettyPrint=false
Name, Major:
Alexandra, English
Andrew, Math
Anna, English
Becky, Art
Benjamin, English
Carl, Art
...
Robert, English
Sean, Physics
Stacy, Math
Thomas, Art
Will, Math

$ tree output/
output/
└── 0001https:@@sheets.googleapis.com@v4@spreadsheets@1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms@values@Class%20Data%21A2%3AE?alt=json&prettyPrint=false

0 directories, 1 file

traceされたファイルがどのようなものか気になる方はgistを参照してみてください(access tokenの部分だけはマスクしてあります)。

実行前に中のコードを触りたくない

:warning: ただしここで注意点があります。元々の出自は書き捨てのコードでした。

何らかの機能が既にコード上に組み込まれていれるなら、環境変数などで指定したりオプションを指定したりすることで実行時の挙動を便利に変更できるのですが(DEBUG=1などが良い例です)、そもそも今回の対象は深入りしたくない書き捨てが出自のコードでした。デバッグ時のための便利な機能など持っているはずがありません。

また、「全てのコードに対してデバッグしやすくするためにxxx用のコードを追加しておくべき」みたいなルールを決めたりするのは些か面倒です(政治的な活動は、好きな人と嫌いな人、得意な人と苦手な人がいます)。必要であればやるべきですが。

冒頭でエラーに遭遇してしまった人のことを思い出してみてください。このプロジェクトに関しては新顔でした。この種の便利な機能に対して、はじめから周囲に自信満々でこれをやるべきなどとコミュニケーションをするより、黙って静かに作ってみた機能が動作するかを試してみたいと思いませんか?(気質の問題かもしれません)。

先程のhttptrace.Patchは一種のモンキーパッチのようなもので、手元のmainのコードに少しのコードを追加すれば良いだけではあるのですが、めんどくさかったりします(怠惰なんです。生まれの問題かもしれません)。

そこで、今回go-run-httptraceというコマンドを用意しました。これをgo runのように使ってもらえるとhttputilによるtrace付きで実行されることになります(ただしgo runとしては機能はサブセットです複数の*.goを指定やパッケージを指定しての実行はサポートしていません)。

例えば、これはexamplesにあるGithubのAPIを叩いたコードの実行の例です。

$ mkdir -p output
$ TRACE=output go-run-httptrace httptrace/_example/github/main.go
2018/12/02 22:09:01 parse httptrace/_example/github/main.go
2018/12/02 22:09:01 transform AST
2018/12/02 22:09:01 format
running via github.com/podhmo/go-traceable/cmd/go-run-httptrace/
2018/12/02 22:09:02 trace to 0001https:@@api.github.com@repos@podhmo@go-traceable@contributors
2018/12/02 22:09:03 rollback httptrace/_example/github/main.go
$ tree output
output
└── 0001https:@@api.github.com@repos@podhmo@go-traceable@contributors

0 directories, 1 file

traceされたファイルがどのようなものか気になる方はgistを参照してみてください。

実装について

内部の実装についてはひどく単純で、go runの前後に処理を挟むようにASTの変換をしているだけです。

前後に処理を挟むとは通常以下の様なコードである部分を

func main() {
    ..
}

以下の様に変えるということです。

func main() {
    doSomethingBeforeMain() // deferが付く場合もある
    mainInner() // 元のmain()
    doSomethingAfterMain()
}

// 元のmain()
func mainInner() {
    ..
}

この状態のコードをgo runで実行し実行後に元のコードに戻しています。

おわりに

「go runの実行をwrapしてhttp/httpsのrequestを手軽にtraceしたい」ということで、以下の2つでhttpsにも対応したtraceの機能を作って紹介してみました。

  • DefaultTransportにパッチを当てるコードの追加
  • go runされるmain()をAST変換

最初は「go runの前後に処理を挟んで実行したい」という題で記事を書こうかと思ったのですが、内部の実装についてくわしく書くよりも何か便利なツールを作りその紹介の方がわかりやすいかなーと思い今回の記事になりました。なので実際には「main()の前後に処理を差し込む」という機構を使って何か面白いことができないか?ということに興味があったりします。何か面白いことを思いついた人は教えてくれたら嬉しいです。

試しに作ってみたgo-run-httptraceについてですが、手軽さという意味で内部でgo runを実行しちゃってます。しかし、magegoaの実装を見ているとgo buildでバイナリを生成してそのバイナリを実行という風になっているのでそちらのほうが良いかもしれません(実行前に変換したコードを元に戻せます)。go run自体も内部ではtmp directoryにbuildして実行していたりしますね。ちなみにinternal packageなどの扱いを考えての手抜きで元のコード自体を変換して上書きするという形式を取っています(もっと良い手順があればそちらに移行したいです)。

あと、実装しやすかったからということでhttputilのdumpの機能を使っていますが、みやすさなどを考えると他の表現の方が見やすいかもしれません。あるいはブラウザ上でリクエストを記録したときなどのように.harの形式でまとめても良かったかもしれません。その他traceという話で言えば、手軽にMITMのproxyを立てるツールキットのようなものを作った方が良いのかもしれません。

ほんとにさいごに

個人的には、コード生成をしたりだとかASTをいじったりみたいなことが好きです。そういう話ができる人や場所を募集してます。twitterなどでmentionなりを飛ばしてくれたりフォローしてくれたりすると嬉しいです。あるいはそのようなslackなりdiscordなりがあれば参加したいです。つまりこの記事の本題は友達募集中という感じのものでした。おしまい。

【小ネタ】すげー古いLinuxサーバーでGo言語を使うときに注意すること

 この記事は Go Advent Calendar 2018 7日目の穴埋め記事です。

シングルバイナリ最高ですよね!

Go4 Advent Calendar 2018 の2日目の記事 で、こんなことを書きました:

いちいち情シスにおうかがいたてなくても、ホームフォルダにコピるだけ使える!シングルバイナリが作れるGo言語最高ですね!

 ちなみに、昔書いた別の投稿 でも同じこと書いてました。
 俺、どんだけシングルバイナリ好きなんだよ・・・?

でも、こんな現象に遭遇したことありませんか?

 ちょうしに乗って色々なサーバにバイナリをコピーして使ってる私みたいな人の中には、こんな目に遭ったことがある人もいるのでは?

myMacbook> GOOS=linux GOARCH=arm64 go build
myMacbook> scp ./kaito centos4.8:/home/myhome
myMacbook> ssh centos4.8
centos4.8> ./kaito -v
futexwakeup addr=0x6d02a0 returned -38
fatal error: unexpected signal during runtime execution
[signal SIGSEGV: segmentation violation code=0x1 addr=0x1006 pc=0x4277fb]

...(中略)...

goroutine 1 [runnable, locked to thread]:
syscall.Syscall6(0x10b, 0xffffffffffffff9c, 0x2a980140a0, 0x2a98058000, 0x80, 0x0, 0x0, 0xffffffffffffffff, 0x0, 0x26)
    /Users/maki/.brew/Cellar/go/1.11.2/libexec/src/syscall/asm_linux_amd64.s:44 +0x5
syscall.readlinkat(0xffffffffffffff9c, 0x59955a, 0xe, 0x2a98058000, 0x80, 0x80, 0x55ab80, 0x56f301, 0x2a98058000)
    /Users/maki/.brew/Cellar/go/1.11.2/libexec/src/syscall/zsyscall_linux_amd64.go:90 +0xbd

...(略)

 なんか盛大にSEGVしてますね。

 Goで作ったバイナリを、あまりに古いLinuxサーバで使うとこういうpanicに見舞われることがあります。こんなときどうすればいいのでしょうか?

答え: Go 1.4でビルドしましょう

 いきなり答えですが、Go 1.4でビルドすることでだいたい解決します。

 古いバージョンの Goツールは、こちらの Archived versions からダウンロードできます。
 また、Mac なら Homebrew で古いバージョンの Go をインストールできます。今回はこちらの方法を使いましょう。

 brew コマンドを使ってインストールします。その際、忘れずにクロスコンパイラのオプションを指定しましょう:

myMacbook> brew install go@1.4 --with-cc-all

 でも、go@1.4は keg-only のため、デフォルトではPATHが通ったところには置かれません。
 なので、今回はフルパスを明示して実行します:

myMacbook> GOOS=linux GOARCH=amd64 ~/.brew/Cellar/go@1.4/1.4.3-20170922/bin/go build

 さて、今度はどうでしょうか?

myMacbook> scp ./kaito centos4.8:/home/myhome
myMacbook> ssh centos4.8
centos4.8> ./kaito -h
Usage:
  kaito [OPTIONS]

Application Options:
  -G, --disable-gzip   Disable Gzip decompression and pass through raw input.
  -B, --disable-bzip2  Disable Bzip2 decompression and pass through raw input.
  -X, --disable-xz     Disable Xz decompression and pass through raw input.
  -n, --force-native   Force to use Go-native implentation of decompression algorithm.
  -c, --stdout         Write the decompressed data to standard output instead of a file. This implies --keep.
  -k, --keep           Don't delete the input files.
  -d, --decompress     Nop. Just for tar command.

Help Options:
  -h, --help           Show this help message

 動きましたね。

なんで Go 1.4 なの?

 上のスタックトレースを見てみると syscall.readlinkat と書かれていることから、どうやら readlinkat システムコールを呼び出そうとして失敗したことがわかります。

 実は Go 1.5 から、Linuxのシステムコール呼び出しが変わっていまして、openat とか readlinkat 等の「○○at」系のシステムコールを使うように標準ライブラリの syscall パッケージが軒並み書き換えられました1
 そして、この「○○at」系のシステムコールは、kernel 2.6.16 で導入された比較的新しいシステムコールなんですね2

 そして、さきほどのエラーを吐いていた「古いサーバ」は、こんな環境でした:

centos4.8> cat /etc/redhat-release
CentOS release 4.8 (Final)
centos4.8> uname --kernel-release
2.6.9-89.EL

 カーネルが2.6.16以前なので、readlinkat システムコールがまだ実装されていなかったというわけです。

 CentOS と RedHat Enterprise Linux (RHEL)だと、この4.x系までが「○○at」系をサポートする以前のカーネルを使っています3
 まぁ、4.x系なんて2011年にサポートが終わっているので、さすがにもう使ってる人なんていねーだろ、と思ったそこのアナタ!・・・・いたんだよ!某社のWebサーバで、ガンガンにエンドユーザーにさらされてるサーバに2年前まで使われてたんだよ!!!!4

結局、Goを使うのに必要なLinuxのバージョンっていくつなの?

 はい、それは明確に決められていまして、(現時点での)Minimum Requirement は kernel 2.6.23 と公式に設定されています
 そしてここには、

We don't support CentOS 5. The kernel is too old (2.6.18).

 って明記されちゃってたりするんですね。

 じゃあ、実際に CentOS 5系でなにか問題があるのか?
 実は、「○○at」系システムコール以外にも、新しめのシステムコールとして epoll_pwait っていうのがあります。
 kernel 2.6.19 からなので、CentOS 4.xはもちろん、kernel 2.6.18 を使っている CentOS 5.x でも使えません。

 なので、こんなプログラムを書いてですね、

package main

import (
    "io"
    "os"
)

func main() {
    r, w, _ := os.Pipe()
    go func() {
        w.Write(([]byte)("Hello, world"))
        w.Close()
    }()
    io.Copy(os.Stdout, r)
}

 これをCentOS 5のサーバーにコピーして実行すると、

myMacbook> GOOS=linux GOARCH=amd64 go build -a
myMacbook> scp ./pipe centos5.11:/home/myhome
myMacbook> ssh centos5.11
centos5.11> ./pipe
runtime: epollwait on fd 5 failed with 38
fatal error: runtime: netpoll failed

runtime stack:
runtime.throw(0x47c243, 0x17)
    /Users/maki/.brew/Cellar/go/1.11.2/libexec/src/runtime/panic.go:608 +0x72
runtime.netpoll(0xc00001e000, 0x0)
    /Users/maki/.brew/Cellar/go/1.11.2/libexec/src/runtime/netpoll_epoll.go:75 +0x215

...(略)

 怒られます。

 しかし、Go 1.4 でビルドして実行すると、

myMacbook> GOOS=linux GOARCH=amd64 ~/.brew/Cellar/go@1.4/1.4.3-20170922/bin/go build -a
myMacbook> scp ./pipe centos5.11:/home/myhome
myMacbook> ssh centos5.11
centos5.11> ./pipe
Hello, world

 ちゃんと動きましたね。

 実は、Go 1.10 からポーリングの実装が epoll_wait システムコールから epoll_pwait システムコールを使うように変わりました。
 なので、CentOS 5系をお使いの方は Go 1.9 までが使えますよ。良かったですね!

 CentOS 5.xだってもう使ってる人なんていないでしょ?と思ったそこのアナタ!
 実はこの5.x世代は、RedHat Enterprise Linux が 2020年11月まで Extended Life Cycle Support (ELS) を提供しているのです!!
 そんなわけで、まだ2年近くは全国のレガシーなサーバールームで元気に稼働し続けているはずです。

 この記事が、全国の古いサーバーを日夜支えている情シスの皆さんの一助になれば幸いです。

謝辞

 この記事の内容は、Go 1.5 がリリースされた2015年頃、ギョームで作っていたコマンドラインツールが何故か動かなくなり、「なんでだよーヽ(`Д´#)ノ ムキー!!」ってなってた時に #Soozy 上の #golang チャンネルで質問したところ、 @mattn さんと @syohex さんに教えていただいたのが元ネタです。
 この場を借りて、あらためて両氏に御礼申し上げます。


  1. 具体的にはこのコミットです: https://github.com/golang/go/commit/e7a7352e527ca275a2b66cc3cafde09836345a8f 

  2. といっても、2.6.16 がリリースされたのは2006年のことなので、今は昔というやつですが。 

  3. ちなみにUbuntuでは同じメジャーバージョンの中でカーネルのバージョンが変わることがあるので一概には言えませんが、6.xくらいまでがあやしいです。 

  4. なお、自分が担当の時代にAWSに移してOSを最新にしたので、さすがにもう稼働していません。こういうサーバは、日本全国にひっそりとたくさんあるはず。それらの保守をしなければならない全国の情シスの皆さんにお見舞い申し上げます。 

The Go Playgroundのちょっとした機能

External article

Go言語で今日傘が必要か教えてくれる傘APIをつくってみた ~Mockテストもしっかりやるよ~

External article

Go + docker で Mysqlを使う(multi-stage builds & docker-composeで)

やりたいこと

普通にイメージを作成するとどうしても大きくなってしまうGoイメージですが、multi-stage buildsにすると
格段にサイズが小さくすることができます。
今回はdocker(docker-compose)で環境を準備し Go から MySQL にアクセスする方法について書いていきます。

基本的に、以前自分が勉強でtodolist用のAPIを作成するために作成したリポジトリからの出典になります。
https://github.com/t0w4/toDoListBackend

Dockerfile

FROM golang:1.11.2-alpine3.8 AS build

WORKDIR /
COPY . /go/src/github.com/t0w4/toDoListBackend
RUN apk update \
  && apk add --no-cache git \
  && go get github.com/go-sql-driver/mysql \
  && go get github.com/google/uuid \
  && go get github.com/gorilla/mux
RUN cd /go/src/github.com/t0w4/toDoListBackend && go build -o bin/todolist main.go

FROM alpine:3.8
COPY --from=build /go/src/github.com/t0w4/toDoListBackend/bin/todolist /usr/local/bin/
CMD ["todolist"]

goのdockerの公式サイトも見ましょう。
docker上では$GOPATHが /go になることも注意しましょう。

docker-compose.yml

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - mysql_data:/var/lib/mysql
      - ./sqls/init:/docker-entrypoint-initdb.d
    ports:
      - "3306:3306"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: gerhuhaer
      MYSQL_DATABASE: todoList
      MYSQL_USER: t0w4
      MYSQL_PASSWORD: faweufhli

  todolist:
    build: .
    depends_on:
      - db
    ports:
      - "8080:8080"
    environment:
      MYSQL_DB_HOST: db
      MYSQL_DB: todoList
      MYSQL_PORT: 3306
      MYSQL_USER: t0w4
      MYSQL_PASSWORD: faweufhli
    restart: always

volumes:
  mysql_data:

注意!!!!!!!
今回特に外部公開もしていないのに勉強用だからいいですが、外部から見えるような場所でID, パスワードをほいほい公開しないようにしましょう。
各環境変数の要不要、意味については公式を読みましょう。下手解説より100倍いいです。

/docker-entrypoint-initdb.d にsqlを配置すると初期起動時に実行してくれるますので、DBやテーブルの作成、初期データの投入などの用途で使えます。
また永続化のためMySQLのデータはローカルにマウントさせています。

GoでのMySQLへのアクセス

GoでMySQLを扱う部分についてはRevelでGORMを使ったMySQL Associationの実装が参考になったので
そちらを見てください。。。。だと不親切なので、以下に自分がORMを使わないで書いた実装があるんで載せます。

package db

import (
    "database/sql"
    "fmt"
    "os"
    "strings"

    _ "github.com/go-sql-driver/mysql"
)

func Init() (*sql.DB, error) {
    connectionString := getConnectionString()
    db, err := sql.Open("mysql", connectionString)
    if err != nil {
        return nil, err
    }
    return db, nil
}

func getParamString(param string, defaultValue string) string {
    env := os.Getenv(param)
    if env != "" {
        return env
    }
    return defaultValue
}

func getConnectionString() string {
    host := getParamString("MYSQL_DB_HOST", "localhost")
    port := getParamString("MYSQL_PORT", "3306")
    user := getParamString("MYSQL_USER", "root")
    pass := getParamString("MYSQL_PASSWORD", "")
    dbname := getParamString("MYSQL_DB", "todoList")
    protocol := getParamString("MYSQL_PROTOCOL", "tcp")
    dbargs := getParamString("MYSQL_DBARGS", " ")

    if strings.Trim(dbargs, " ") != "" {
        dbargs = "?" + dbargs
    } else {
        dbargs = ""
    }
    return fmt.Sprintf("%s:%s@%s([%s]:%s)/%s%s",
        user, pass, protocol, host, port, dbname, dbargs)
}

差分は、initの部分と、参考記事が設定ファイルから取得していたところを環境変数から取得するようにしたところくらいです。
自分が開発していた時は、このgetConnectionStringが返すMySQLの接続情報がわからず時間を食ってしまいました。。。

終わりに

記事としてはとても短いので「えっ!これだけ?」と思われるかもしれないですが、意外と上記の内容がまとまった記事というのが見つからなかったので、今後Goをdockerで作っていきたい人たちの参考になれば嬉しいです。

golangでesaのクライアントを作っています。

External article

GoでDialogsを使ったSlack Appを作る

External article

Go Conferenceに参加したぞというアレ

External article

google/wireを使ったDIとDI関数のシグネチャについて

External article

Go+SAMでLambda Layersのテンプレートを作成

GoでLambda Layers

re:Invent2018で発表されたLambda Layersですが、どのサイトを見てもPythonの記事ばかりなので、実際にGoを使ってLambda Layersに挑戦してみます。

環境準備

SAMがインストールされていることを確認します。ローカル環境でライブラリのインストールがうまくいかない場合や、Windowsを使用しているけどLinuxコマンドを使ってやりたい場合は、Cloud9上で開発すると便利です。ローカルPCのモジュールが肥大化しすぎないという利点もあります。

samのバージョン確認
$ sam --version
SAM CLI, version 0.8.1

※仮にSAMがない場合は、pipコマンドでインストールします。

pipでsam-cliをインストール
$ pip install aws-sam-cli

Goプロジェクトの作成

sam initを使用して、Goのプロジェクトを作成していきます。
今回は「cancer」という名前のプロジェクト名にします。

プロジェクト作成
$ sam init --runtime go1.x --name cancer
$ ls
cancer  README.md

デフォルトの状態からビルドして実行確認をしていきます。
Makefilebuildコマンドがデフォルトで用意されているため、そのまま利用していきます。

ビルド実行
$ cd cancer/
$ ls
hello-world  Makefile  README.md  template.yaml
$ make build
GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

※cannot find packageエラーが発生したら、go getでインストールをしていきましょう。

パッケージインストール
$ go get "github.com/aws/aws-lambda-go/events"

動作確認

ビルドが完了したら、APIをローカルで実行してみます。

APIのローカル起動
$ sam local start-api

動作確認してみましょう。
ターミナル画面から「+」ボタンを押し、「New Terminal」を選択してcurlコマンドを実行します。

動作確認
$ curl http://127.0.0.1:3000/hello
Hello, 54.237.36.52

ここまでで、通常のGoプロジェクトテンプレートが完成しました。

Layer側のGoプロジェクトを作成

Layersのプロジェクトを作成するために、まずはディレクトリを作成しましょう。

Layerプロジェクトのディレクトリ作成
$ mkdir layers
$ cd layers
$ mkdir first-layer
$ cd first-layer
$ pwd
/home/ec2-user/environment/cancer/layers/first-layer

次に、第一レイヤであるfirst.goを作成していきます。

first.goの作成
$ vi first.go
first.go
package main

import (
  "fmt")

func main() {
  fmt.Println("Hello, First Layer!")
}
layerの実行確認
$ go run first.go
Hello, First Layer!

Lambdaにソースコードをアップロードするため、ビルドモジュールをzipに固めます。

zipの作成
$ GOOS=linux GOARCH=amd64 go build -o first
$ zip first.zip ./first
$ ls
first  first.go  first.zip

その後、S3にzipファイルをアップロードします。今回は「sam-template-store」というバケットを既に作成しているので、こちらにアップロードしていきます。バケットを作成していない場合はコンソールから作成しましょう。

S3にzipファイルをアップロード
$ aws s3 cp ./first.zip s3://sam-template-store/ --acl public-read                          

YAMLでレイヤのデプロイ

template.yamlを使用して、レイヤを作成していきます。
既にアップロードされたS3のzipをContentUriに指定しましょう。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  cancer

  Sample SAM Template for cancer

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 5

Resources:
  FirstLayersFunction:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: FirstLayer
      Description: First Layer
      ContentUri: 's3://sam-template-store/first.zip'
      CompatibleRuntimes: 
        - go1.x

Outputs:
  LayerVersionArn:
    Value: !Ref FirstLayersFunction

CloudFormationのコマンドでデプロイします。

レイヤのデプロイ
$ pwd
/home/ec2-user/environment/cancer
$ aws cloudformation deploy --stack-name first-layer --template-file template.yaml

Successfullyがターミナルに表示されたら、レイヤのArnを取得するため、以下コマンドを実行します。

レイヤのArn取得
$ aws cloudformation describe-stacks --stack-name first-layer
...
        "Description": "cancer\nSample SAM Template for cancer\n", 
            "Tags": [], 
            "Outputs": [
                {
                    "OutputKey": "LayerVersionArn", 
                    "OutputValue": "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:FirstLayer:3"
                }
            ]
...

OutputValueで表示されたArnを下記で使用していきます。

レイヤをLambdaFunctionに追加

ここで、HelloWorldFunctionを作成し、Layersとして先ほど作成したLambdaを指定します。
template.yamlに以下を追記していきましょう。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  cancer

  Sample SAM Template for cancer

Globals:
  Function:
    Timeout: 5

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello-world/
      Handler: hello-world
      Runtime: go1.x
      Tracing: Active
      Events:
        CatchAll:
          Type: Api
          Properties:
            Path: /hello
            Method: GET
      Layers:
        - "arn:aws:lambda:us-east-1:xxxxxxxxxxxx:layer:FirstLayer:3"
  FirstLayersFunction:
    Type: AWS::Serverless::LayerVersion
    Properties:
      LayerName: FirstLayer
      Description: First Layer
      ContentUri: 's3://sam-template-store/first.zip'
      CompatibleRuntimes: 
        - go1.x

Outputs:
  LayerVersionArn:
    Value: !Ref FirstLayersFunction

samコマンドでパッケージングとデプロイを行います。
Makefileに記載すると打ち込むコマンドが少なくなるので便利です。

Makefile
.PHONY: deps clean build

deps:
    go get -u ./...

clean: 
    rm -rf ./hello-world/hello-world

build:
    GOOS=linux GOARCH=amd64 go build -o hello-world/hello-world ./hello-world

# 追記    
package:
    sam package --template-file template.yaml --output-template-file output-template.yaml --s3-bucket sam-template-store 

# 追記
deploy:
    sam deploy --template-file output-template.yaml --stack-name sam-template-store --capabilities CAPABILITY_IAM

Makefileでデプロイまで実行してみましょう。

pakageの作成
$ make package
sam package --template-file template.yaml --output-template-file output-template.yaml --s3-bucket sam-template-store 
Functionのデプロイ
$ make deploy
sam deploy --template-file output-template.yaml --stack-name sam-template-store --capabilities CAPABILITY_IAM

最後に、Lambdaのコンソールから状況を確認してみます。
HelloWorldFunctionにLayersとして、先ほど作成したFirstLayerが参照されてことが確認できました。

FireShot Capture 116 - Lambda Management Console_ - https___console.aws.amazon.com_lam.png

Golangでログを吐くコツ

External article

gocode やめます(そして Language Server へ)

External article

Windows 上の go-gl で Cgo を不要にしようとしている話 - なぜ Syscall18 が生まれたか

tl;dr

go-gl は Go の OpenGL バインディングです。 Windows などのデスクトップ環境に対応しています。実装は単純に C の関数を Cgo を使って呼ぶだけです。が、 Cgo には後述するような問題があり、現在のところ必要悪とみなされています。

ところで Windows では、 DLL からの関数ポインタ取得および syscall.Syscall などの関数を使うことで、一切 Cgo を使わずに C 関数を利用することができます。自分はこの修正を提案し、無事アクセプトされました。現在 PR がレビュー中です。

提案の詳細は Design Doc にまとめています。本記事はこの提案のざっくりとした和訳および補足説明です。

背景

OpenGL は C の関数セットであるため、 Go と OpenGL のバインドに Cgo が使われています。しかしながら Cgo には以下のような問題があります:

  1. Cgo は C コンパイラを必要とする。 1
  2. Cgo はコンパイルを遅くする。 2
  3. Cgo はクロスコンパイルを困難にする。 3

これらは特に Windows ユーザーにとっては負担です。ほかの POSIX な環境とは異なり、 C コンパイラに馴染みがないかもしれないからです。幸運なことに、 Windows では syscall.Syscall 関数を使って C 関数を Cgo なしで呼ぶことができます。残念なことにこの手法は他のプラットフォームでは困難です。

筆者は glow (バインディングのジェネレータ) を修正し、 Windows では syscall.Syscall を使って Cgo に依存しないようにする提案をしました。

コードの変更

glow は 4 つのファイルを出力するので、説明も 4 パートに分けます。

conversions.go

プロトタイプ実装

Go と C の値を相互変換する関数など。筆者の見る限り Cgo を一切使わなくても同等のことができるので、そのように修正します。

debug.go

プロトタイプ実装

Go の関数を C から呼び出す必要があるため、 syscall.NewCallback を使うという修正をしました。

というか現在そもそもこの関数まわり、全く動いていない気がするんですよね。誰も使ってないのでは??

procaddr.go

プロトタイプ実装

関数名から関数ポインタを取得する実装は Cgo を使わずに syscall.NewLazyDLL などを使えばいけるので、そのように修正します。

package.go

プロトタイプ実装

一番作業量が多い部分。基本的に syscall.Syscall を呼ぶように修正するだけなのですが、引数の数によって syscall.Syscall6 だったり syscall.Syscall9 だったり、呼ぶ関数が変わってしまいます。その修正を行います。

syscall.Syscall 系の関数は引数をすべて uintptr で受け取ります。整数型など、たいていそのままコンバートすればいいのですが、一部例外があります。 bool はそのための if 文が必要だったり、浮動小数点型は math.Float64bits なで変換してあげる必要があります。

Proof of concept

実際提案だけだと絵に描いた餅なので実際に example を動かしてみました。詳細な手順は Design Docs を参照してください。

screen.png

後方互換性

ほとんどの関数はそのまま動くことが期待されますが、現時点でどうしても動かない関数が 2 つあります:

  • func LGPUCopyImageSubDataNVX(sourceGpu uint32, destinationGpuMask uint32, srcName uint32, srcTarget uint32, srcLevel int32, srcX int32, srxY int32, srcZ int32, dstName uint32, dstTarget uint32, dstLevel int32, dstX int32, dstY int32, dstZ int32, width int32, height int32, depth int32)
  • func MulticastCopyImageSubDataNV(srcGpu uint32, dstGpuMask uint32, srcName uint32, srcTarget uint32, srcLevel int32, srcX int32, srcY int32, srcZ int32, dstName uint32, dstTarget uint32, dstLevel int32, dstX int32, dstY int32, dstZ int32, srcWidth int32, srcHeight int32, srcDepth int32)

syscall.Syscall 系の関数は引数の数ごとに関数名が異なりますが、最大が syscall.Syscall15 であり、 15 個が最大値です。しかしながらこれらの関数は 16 個以上引数を取ります。よって syscall.Syscall の手法ではこれらの関数は呼べない、ということになります。

この手法の唯一の懸念点だったのですが、「まあこの関数、誰も使ってないでしょ」ということで、未実装状態で置いとくということで、一件落着しました。

ついでながら syscall.Syscall18 を提案し、実装してしまいました。 Go 1.12 から使えるようになります。よかったですね!

現在の状況

PR を提出しましたが、現在レビュー中です。結構中の人が忙しいようです。早ければ年内にもマージされるでしょう。

あとは「これを入れるとしてメンテする人いるの?」という問題があって、「じゃあ自分がやります」と立候補したところ、 go-gl のメンバーに入れていただきました。今後とも宜しくお願いします。

ちなみに拙作の Ebiten では修正が適用された go-gl を先行して独自に使用しています。よって少なくとも go-gl 部分については Cgo 依存がなくなりました。


  1. 実際に筆者は、 Windows に C コンパイラを入れるのに苦労している人々を何人も見てきました ()。つらい。ちなみに筆者は scoop を使って入れてます。 

  2. 筆者の Windows 環境では、 Cgo を用いた go-gl コンパイルは数分を要しました。なお Cgo を使わないバージョンの go-gl のコンパイルは 1 秒未満です。 

  3. MinGW をいれたりなど、いろいろな工夫をすれば Linux から Windows へのクロスコンパイルは可能。だが、 Pure Go であることに越したことはないでしょう。 

Goでxo/xo入門

External article

Goで作るP2Pライブラリ

TL;DR

  • Goはネットワークライブラリを書くのに非常に良かった
  • libp2pやIPFSに期待

はじめに

はじめにお断りしておくとあまり実用的な話題ではありません・・・。
以前GoでP2Pファイル転送コマンドを作ったという記事を書いたときにも結構触れたのですがP2Pの仕組みを個人的に勉強したい、またライブラリを作ってGoで使いたい、という思いになり、P2Pネットワークライブラリを実装してみました。
より実装寄りの話をしたいと思います。

技術選択

P2Pで接続するにおいて一番の難所となるのはやはりNATです。ここらへんをうまく突破して接続するための方法をまず検討しました。
といってもどうするのが最適かイマイチわからなかったのでWebRTCを参考にしました。(WebRTCは最近のWebブラウザについているブラウザ同士でP2Pで動画やデータを流すやつです)

具体的な接続ステップを簡単に示すと

  • UDPのポートをlistenする
  • 上で開いたポートに紐づけられたIPアドレス、ポートの取得をする
    • ローカルのIPアドレスをリストアップ(LAN内など)
    • STUNを使ってインターネットに対して公開されるIPアドレス、(NAPTの場合は)ポートを取得
  • (それでもできない環境向けにTURNサーバ経由での接続環境を用意)
  • 接続するノード同士で接続情報をシグナリングサーバ経由(あるいは手動)で交換
  • 交換された接続情報に対して接続を試みる

いわゆるUDPホールパンチングという方法ですね。実装もそこまでつらくなくてわかりやすくて良い感じです。
問題は接続確立後にファイル転送とかしようにもパケロスでデータが破損したら困ります。再送制御とかまで自分で実装するのは嫌だったのでライブラリを探し、QUICを採用することにしました。(最初はµtpというプロトコルを採用していたのですが後にそのライブラリがdeprecatedになってしまったので代替を探しました)
QUICは最近IETFにおいて標準化が進行しているプロトコルで、HTTP/3という名称1になるようです。 今回はP2Pでつなぐ2つの機器の一方をQUICサーバ、もう一方をQUICクライアントとしました。

net.PacketConn

Goのnet packetにはPacketConnというinterfaceがあります。
TCPでは1対1の通信しかできないので

Read([]byte) (int, error)
Write([]byte) (int, error)

のようになっていますが、UDPでは不特定多数と通信できるため以下のようになっています。

type PacketConn interface {
    ReadFrom(p []byte) (n int, addr Addr, err error)
    WriteTo(p []byte, addr Addr) (n int, err error)
    Close() error
    LocalAddr() Addr
    SetDeadline(t time.Time) error
    SetReadDeadline(t time.Time) error
    SetWriteDeadline(t time.Time) error
}

既存のライブラリのベースとなっているソケットとうになんらかの処理を加えるのは他の言語だと難しい場合も多いですが、Goではquic.Listenquic.Dialの引数に渡すPacketConn interfaceを実装したstructを自分で用意するだけで済みます。これもGoのinterfaceの仕組みや、標準ライブラリにinterfaceが豊富に予め定義されているおかげだと思います、非常にありがたかったです。

sharablePacketConn

今回用いたQUICはTCPと同じような感覚のため、クライアントはサーバと1対1で通信することが想定されています。ですが接続先の候補となるIPアドレス・ポートのペアは複数存在するためそれに対して順番に接続を試しているとめちゃくちゃ時間がかかります。これを防ぐためには1つのポートのPacketConnで複数のサーバと通信できる必要があります。

そのためにsharablePacketConnをまず作りました。Register関数に使いたいアドレスを渡すとそのアドレスと通信するためのchildPacketConnを返します。それを使って通信する仕組みになっています。また、接続確立後も複数のサーバと通信する仕組みとなっている必要はなく、オーバーヘッドとなってしまうため、1つのchildPacketConnのみにしてそれ以外の通信を弾く機構をつけました。

サーバ側のファイアウォール対応

また、サーバ側もただ待っているだけでよいかというとそうでもないことが多いです。セキュリティ的な問題でファイアウォールが一度もパケットを送信していないところからの受信を制限している場合があります。こういう環境に備えてサーバ側からクライアントに対して適当な小さなパケットをあらかじめ飛ばし、そのパケットはクライアント側で無視するようにしました。

認証機構

接続が完了してもそれが正しい通信相手であると保証できていません。それを確かめるための認証機構が必要です。MITM攻撃などが成立しては困ります。
今回は認証機能をまるごとquic-goに任せてしまいました。QUICはTCP+HTTP2+TLSを置き換えるようなものなので公開鍵認証がついています。Goでオレオレ認証局と証明書を発行する方法がよくわからず、色々と調べながら書きましたが何か問題があるかもしれません。これです。ここで生成した認証局の公開鍵を通信相手にIPアドレス・ポートのペアと共に渡すことで認証します。
また、TLSではクライアント認証も行うことができるため同様の方式でサーバ認証もクライアント認証もできてしまいます

実装

あとは頑張って実装する、というお話なので特に話すことがありません・・・。そんなこんなでなんとか実装したのですが、一つの関数に載っている処理量が多すぎるためそれを分割したり、テストコードを書いたりしたいと思っているのですがなかなかできていません、これから頑張ってテストコード書きます。また、せっかくQUICを採用したにもかかわらず、前のµtpと方針を変えていないためマルチストリームに対応できていないのも対応したいです。

余談

IPFS/libp2p

上に載っけた記事にも書いたようにキーワードを交換するだけで簡単にファイルを共有できるコマンドとかを作ったりリバースプロキシを作って自分で使って勝手に満足していたのですが、このライブラリが完成に近づいてきた頃libp2pの存在を知りました。
libp2pはIPFSプロジェクトの一部?のようです。IPFSはInterPlenetary File System、惑星間ファイルシステムという非常にロマンあふれるものでして、要は静的ファイルをモダンなP2Pネットワークで分散管理しよう、というやつ。
先日、静的なある程度の容量のあるファイルをどうやって公開するか悩んだ時ふと思い出してIPFSを試してみたのですが思いのほか使いやすくて良かったです。このIPFSも基本的な実装は全てGoで書かれています。
libp2pではtransportとしてquicが使えるようなので試してみたいと思ったのですが、おそらく最近quic-goがGoogle実装からIETF実装へ切り替えた影響でビルドが通りませんでした。

WebRTC

WebRTCはJavaScriptとC++、それとモバイル端末向けライブラリが多く他の言語で使われている例があまりありません。ですが最近GoでWebRTCを(libwebrtcへの依存なく)実装しているライブラリ、pions/webrtcが開発されています。libwebrtc以外の実装が現れるのはとても良いことだと思うので期待したいです。

おわりに

あまり中身のない内容となってしまいましたが、ここまでお読みいただきありがとうございました。最近再び注目を集めているP2Pが今後発展していって欲しいと思っています。

正しさとGo

はじめに

Goの良いところは、最低限の文法を知っていればコードを上から順番に読むことで詳細を容易に理解できることです。
文法の中にシンタックスシュガーや特別な省略が許されていないため多様な表現になることはありません。

そのためGoを書ければGoの本体と標準ライブラリを読むことができます。

しかし以下の原因により、これらの利点を守ることが難しくなることがあります。

  • DSL
  • フレームワーク
  • 抽象化

これらは設計として新たな制約を課すことで品質向上や実装を容易にするためのものです。
またこれらを採用する論理立てた 正しい 理由が存在します。

DSL

DSLを提供するツールとして、DIのための wire があります。

GoでDIを実現するためには多くの実装を必要とするため、実装量を減らすためにもDIツールが求められてきました。

これは 正しい です。

しかし一方でDSLはコードを読む人間に言語以上の知識を求めます。
また詳細な挙動をすべて理解して追うことは難しいです。恐らく wire の内部実装をすべて理解して利用している人は少ないでしょう。

フレームワーク

Goの net/httpパッケージは一般的なWebフレームワークに比べて機能が少ないためコードが冗長になります。
そのためWebフレームワークが求められてきました。

これは 正しい です。

しかし一方でWebフレームワークはコードを読む人間に言語以上の知識を求めます。
ときには標準ライブラリのnet/httpとの接続が難しくなることもあります。
また詳細な挙動をすべて理解して追うことは難しいです。恐らく ginecho などの内部実装をすべて理解して利用している人は少ないでしょう。

Webフレームワーク以外にもORマッパーやテスト用のアサーションツールなども同様です。

抽象化

テスタビリティやSOLID原則のために抽象化することがあります。
Clean ArchitectureのInput Port、Output PortやDDDのレイヤードアーキテクチャのRepositoryなどもこれに該当します。

これは 正しい です。

しかし一方で抽象化は本質的なロジック以外のコードが増え、コードの可読性を落とすことがあります。
またどうしても完全な動作を理解しようとしたときは抽象化された先の実装を追う必要がでてきます。

正しさと悪

これらの制約の 正しさ は、正しいため反論をすることは難しいです。
またDSL、フレームワーク、抽象化もGoで実装されているため勉強して理解しないことを とすることが簡単にできます。

しかしこれには毅然として立ち向かう必要があります。

正しさとGo

私は、Goは制約の代わりに読みやすさでコード品質を担保することに比重をおいた言語だと考えています。

例えばJavaでは、検査例外により呼び出し側にエラーハンドリングをしなければいけないという制約を課しました。
Goでは必ずerrorを返すだけです。それを _ で捨てることや受けないこともできてしまうため制約がとても弱いです。しかしほとんどの場合はコードレビューで気がつくことができます。

これだけを聞くと、「コードレビューは見逃す可能性があるのだから、正しく設計をすることでコンパイル時に気がつけるようにするべきだ」 となり、Goの姿勢はエンジニアとして正しくないように感じます。
そのため他の言語を主軸に置いている人からの批判がGoではどうしても多くなりがちです。

しかし正しさを求めることは 制約の代わりに読みやすさでコード品質を担保する ということから離れていくことでもあります。

これらを両立してコーディングしていくことがGoを書く上では、他の言語以上に大切になります。

フレームワークを採用しないことが正しいのか

では、フレームワークを採用しないことが正しいのでしょうか?

以下の場合は採用すべきです。

  • 学ぶ範囲を適切に絞れているツールである
  • ツールの価値が学習コスト以上にある
  • 適切な代替手段がない
GRPCは、Protocol Buffersで記述したルールに基づきコードが生成されます。このルールを覚えることは容易です。
またMicroservicesの文脈では、GRPC前提であることが多いため適切な代替手段がありません。

そのため採用するべきです。

と言うことはできますが、他のWebフレームワークなどでも同様の 正しさ を主張することは可能でしょう。

そのため状況と程度の問題であり、最終的にはアーキテクトのセンスに委ねられます。

まとめ

あるGo本体に近いレイヤーでコードを書いている人から、この 正しさ を嫌う意見を聞いたことが、この話を考え始めたきっかけです。
逆に他の言語でWebアプリケーションを書いてきた方は、この 正しさ を好む傾向にあると思います。
私はどちらの意見も間違ってはいないと思います。

ただしインターネット上にはこの 正しさ が溢れており、新しい技術や目の前の課題を解決してくれるツールは素晴らしく見えます。
そこで新しいツールを導入する際には、一度立ち止まりGoとはこのような言語であったということを思い出し、課題設定が正しいかを改めて考え直してみてください。

それを踏まえてプロダクトと組織のことを考え、長期的に負債を生まないように適切な解決手段を提案しなければなりません。

これらのバランスを考え技術選定できることがアーキテクトの実力の見せ所であり、本当のスキルではないでしょうか。

蛇足

  • 今回はDSL、Webフレームワーク、抽象化に対してどのように対応したかを書いていませんが、また別の機会があればそれぞれ別の記事を書こうと思います。その正しさについて別の課題設定をすることで解決しています。
  • この話は別の視点で考えると、読むコストと書くコストのどちらに比重をおくべきかの議論なのかもしれません。

dept を使った Go ツールの依存管理

External article

GoでSSH Managerを作成した際の知見

External article

Go言語は沼

Go言語入門者である私が気づいたことを長々と書いています。
既に他の方が言及されていることも多いです。また初心者でよくわかっていないことも多いためお手柔らかにお願いします。

なお、順番は適当です。

Go Advent Calendar 2018 24日目の記事として投稿させていただいております。
(元々の方が投稿されていなかったようなので、代わりに入れさせて頂きました。)

継承の代わりとして匿名フィールドを用いた場合、型の判定がうまくいかない

Go言語はオブジェクト指向言語ではありませんが、構造体やレシーバを用いることでオブジェクトのメンバを「呼び出す」ことができます。
まず、Animal「クラス」を作ってみましょう。そして自己紹介するためのレシーバDescribe()も定義します。

type Animal struct {
    Age int
}
func (animal *Animal) Describe() {
    fmt.Printf("I am %v years old.\n", animal.Age)
}

こんどはPerson「クラス」を追加しましょう。人間は動物なのでPerson「クラス」ではAnimal「クラス」を継承したいですね。Go言語では匿名フィールドという機能を使えば、Person構造体にAnimal構造体の機能も持たせることができます。

type Person struct {
    Animal
    Name string
}
func (person *Person) Describe() {
    fmt.Printf("I am %v, %v years old.\n", person.Name, person.Age)
}

ついでにPlant(植物)も定義します。植物には口がないため自己紹介はできません。

type Plant struct {}

ここまでできたら、試しにAnimalとPerson, Plantのオブジェクトをそれぞれ生成して自己紹介させてみましょう。

func callDescribe(obj interface{}) {
    switch obj.(type) {
    case *Animal:
        (obj.(*Animal)).Describe()
    default:
        fmt.Println("It is not an animal.")
    }
}
func main() {
    var animal interface{} = &Animal{10}
    var person interface{} = &Person{Animal{20}, "Joe"}
    var plant  interface{} = &Plant{}
    callDescribe(animal)
    callDescribe(person)
    callDescribe(plant)
}

実行すると以下のようになります。

I am 10 years old.
It is not an animal.
It is not an animal.

動物であるはずの人間が「人間でない」と判定されてしまいました。

解決法

対象が動物かどうかを判定するときに、インターフェースを使えばうまくいきます。

type LooksLikeAnimal interface{
    Describe()
}
func callDescribe(obj interface{}) {
    switch obj.(type) {
    case LooksLikeAnimal:
        (obj.(LooksLikeAnimal)).Describe()
    default:
        fmt.Println("It is not an animal.")
    }
}

実行結果

I am 10 years old.
I am Joe, 20 years old.
It is not an animal.

期待通りに表示されました。

レシーバを使う際に注意が必要な場合

以下のコードを見てください。

type Animal struct { }

func (animal *Animal) DescribeP() {
    fmt.Println("I am a pointer of animal.")
}

func (animal Animal) Describe() {
    fmt.Println("I am animal.")
}

func main() {
    var animal Animal = Animal{}
    var panimal *Animal = &Animal{}
    animal.Describe()
    animal.DescribeP()
    panimal.Describe()
    panimal.DescribeP()
}

実行結果は以下のようになります。

I am animal.
I am a pointer of animal.
I am animal.
I am a pointer of animal.

どうやらGo言語のレシーバはポインタ型でもそうでなくても同じ働き(値渡しと参照渡しという違いはありますが)ができるようです。
(ちなみに func (animal *Animal) Describe()を追加で宣言するとmethod redeclared: Animal.Describeエラーになります)

では、main関数を以下のように書き換えて試してみます。

func main() {
    var panimal *Animal = nil
    panimal.DescribeP()
    panimal.Describe()
}

出力はこのようになります。

I am a pointer of animal.
panic: runtime error: invalid memory address or nil pointer dereference

1つ目のポインタレシーバの呼び出しはうまく行きましたが、2つ目の呼び出しは実行時エラーで失敗しました。
考えてみれば当たり前なのですが、ポインタでないレシーバを使う場合は気をつける必要があるようです。

また以下のような場合はどうでしょうか。

func main() {
    var animal Animal
    var i interface{} = animal
    animal.DescribeP()      // ok
    i.(Animal).Describe()   // ok
    i.(Animal).DescribeP()  // err: cannot take the address of i
}

今度はコンパイルが通らなくなってしまいました。これも技術的な制約であり十分理解できるのですが、やはりポインタレシーバを使う場合も気をつけなければいけないようです。

参考
https://skatsuta.github.io/2015/12/29/value-receiver-pointer-receiver/

Arrayの長さ省略表現

Go言語では配列の宣言及びコピーは以下のようにできます。

func main() {
    primes := [6]int{2, 3, 5, 7, 11, 13}
    var arr [6]int = primes
    fmt.Println(arr)
}

しかしながら、最初のprimesの宣言はすこし冗長です。例えばC言語の場合、配列の宣言と初期化を同時に行う場合は以下のように要素数を省略できます。

int primes[] = {2, 3, 5, 7, 11, 13};

Go言語で同じことをやるとどうなるでしょうか。

func main() {
    primes := []int{2, 3, 5, 7, 11, 13}
    var arr [6]int = primes
    fmt.Println(arr)
}
cannot use primes (type []int) as type [6]int in assignment

エラーになってしまいました。

解決法

Go言語で配列宣言時の要素数を省略する場合は...を用います。

func main() {
    primes := [...]int{2, 3, 5, 7, 11, 13}
    var arr [6]int = primes
    fmt.Println(arr)
}

スライス

(2019.4.15 追記)

先程の例のprimes := []int{2, 3, 5, 7, 11, 13}は配列ではなくスライスの初期化を意味します。
以下に示すように、Go言語では配列よりもスライスを活用すると便利です。

    // 長さが0のスライス
    var a = make([]int, 0)
    a = append(a, 12)

    // 最初から初期化されているスライス
    var b = []int{1, 1, 2, 3, 5}
    fmt.Println(a, b)   // 出力: [12] [1 1 2 3 5]

エラーハンドリング

Go言語にはtry...catchのようなエラー処理機構がありません。
......本当はある(panic)のですが、通常は使うことが推奨されません。(回復処理を期待できない場合のみ使う)

参考: https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right

In panicing you never assume that your caller can solve the problem. Hence panic is only used in exceptional circumstances, ones where it is not possible for your code, or anyone integrating your code to continue.

よって、エラーは基本的に全て戻り値で返します。例:

package main
import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    defer file.Close()
}

では、さらにディレクトリを作成したくなった場合、main関数をどのように書き換えればよいでしょうか。

func main() {
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    err := os.Mkdir("hoge", 0777)
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    defer file.Close()
}

これは一見正しそうですがコンパイルエラーです。

no new variables on left side of :=

Go言語で:=は変数の宣言と代入を同時に行ってくれる演算子ですが、変数は二重に宣言することができないため、二回目にerr := ...としたところでエラーになります。よって、二回目の:==に書き換えれば良いです。

func main() {
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    err = os.Mkdir("hoge", 0777)
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    defer file.Close()
}

しかし、別の解決策もあります。実はこの例では、ファイルとディレクトリの作成順を入れ替えると:=を使用したままでもエラーが発生しなくなります。

func main() {
    err := os.Mkdir("hoge", 0777)
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    file, err := os.Create("test.txt")
    if err != nil {
        fmt.Fprintf(os.Stderr, "File create error\n")
    }
    defer file.Close()
}

Go言語の仕様上、複数の変数に対し:=で代入するとき、2つ目以降の変数については既に存在していてもエラーにならないのです。(型が合わないときはエラーになります)

上の例ではエラー処理を2回行っているのですが、少し面倒ですね。これを1回で済ます方法はないでしょうか。

func main() {
    err := os.Mkdir("hoge", 0777)
    if err == nil {
        file, err := os.Create("hoge/test.txt")
        if err == nil {
            file.WriteString("abc")
            defer file.Close()
        }
    }
    if err == nil {
        fmt.Println("Success!")
    } else {
        fmt.Fprintf(os.Stderr, "%v\n", err)
    }
}

エラー処理を最後にまとめてみました。ついでにファイルへの書き込みも行っておきました。これは一見うまく機能するように見えます。
では意図的にエラーを発生させてみましょう。4行目をfile, err := os.Create("fuga/test.txt")とすれば、書き込み先のディレクトリが存在しないためエラーになるはずです。

$ go run test.go
Success!
$ ls hoge
.  ..
$ ls huga
ls: 'huga' にアクセスできません: そのようなファイルやディレクトリはありません

エラーになりませんでした。しかしながらもちろんファイルは作成されていません。なぜでしょうか? ぜひ考えてみてください。

番外編: Go言語でtry...cacheに近いことをやる

先程Go言語にはtry...cacheが無いと書きましたが、似た仕組みはあります。それがpanicです。
では、panicを使用してどエラーを起こすにはどのようにすればよいのでしょうか。

func errFunc(name string) {
    panic(name + " does not want to do anything.")
}

func main() {
    errFunc("Bob")
    fmt.Println("Success")
}
$ go run test.go
panic: Bob does not want to do anything.

panicによってプログラムが強制終了されました。ではこれをcacheするためにmain関数を書き換えます。

func main() {
    defer func() {
        fmt.Printf("recovered from panic: ")
        fmt.Println(recover())
    }()
    errFunc("Bob")
    fmt.Println("Success")
}
$ go run test.go
recovered from panic: Bob does not want to do anything.

うまくcacheできています。ですが、"Success"が表示されません。それは、main関数全体がいわばtry...catchtry節のようになっているからです。では、finallyを実現してみましょう。

func main() {
    func(){
        defer func() {
            fmt.Printf("recovered from panic: ")
            fmt.Println(recover())
        }()
        errFunc("Bob")
    }()
    fmt.Println("Success")
}
$ go run test.go
recovered from panic: Bob does not want to do anything.
Success

一応うまくできました。しかし見た目が気持ち悪いですね。実際に使う場合はtry節の内部のみを別の関数として宣言した方がよさそうです。

for range

Go言語にも、foreachのような構文が用意されています。それがrangeです。

func main() {
    for p := range [...]int{2, 3, 5, 7, 11} {
        fmt.Println(p)
    }
}

実行結果:

$ go run test.go
0
1
2
3
4

期待した結果にはなりませんでした。正しくは以下のようにします。

func main() {
    for _, p := range [...]int{2, 3, 5, 7, 11} {
        fmt.Println(p)
    }
}

実行結果:

$ go run test.go
2
3
5
7
11

小さな違いが大きなバグを生む典型例です。

セミコロン自動挿入による文法制約

Go言語で複雑な計算をしたいとします。あまりにも複雑なため1行で収まらず、下のように途中で改行を入れました。

func main() {
    a := 2 + 3 + 5 + 7 + 11
        + 13 + 17 + 19
    fmt.Println(a)
}
$ go run test.go
# command-line-arguments
./test.go:7:16: +13 + 17 + 19 evaluated but not used

コンパイルが通りませんでした。しかし、以下のように書き換えるとうまくコンパイルできます。

func main() {
    a := 2 + 3 + 5 + 7 + 11 +
         13 + 17 + 19
    fmt.Println(a)
}

なぜこのようになるかというと、Go言語では各行末に自動的に文の終わりを示す;を挿入しているからです。2つ目の例がうまくいったのは、行の末尾が記号で終わる場合は;を挿入しない、という簡単なルールによって制御されているからです。どこかのes6とは大違いですね。(実際はそこまで単純ではないようですが)
すなわち、単純な制御構文の例でも同じことが起こります。

func main() {
    a := 2
    if a != 1    // コンパイルエラー
    {
        fmt.Println("a is not 1")
    }
}

Tclみたい

名前空間とパッケージ名

Go言語でプログラムを書くとき、最初にpackage mainと記述します。これは、「このファイルはmainパッケージに属している」という意味ですが、main以外のパッケージを作って名前空間を分けたい場合はどのようにすればよいのでしょうか。Go言語ではこのパッケージ名はディレクトリ階層に対応しています。

$ tree
.
├── main.go
└── pub
    └── sub
        └── file.go

2 directories, 2 files
$ cat pub/sub/file.go
package sub

import "fmt"

func Sub() {
        fmt.Println("Hello from sub")
}
$ cat main.go
package main

import "./pub/sub"

func main() {
        sub.Sub()
}

上の例を見てください。パッケージ名はファイル名ではなくディレクトリ名と対応していることがわかります。

interface{} が nilにならない

Go言語においてあらゆる値を代入できる方としてinterface{}があります。
そんな便利なinterface{}ですが、一度nilを代入すると大変な厄介者に...。

type A struct {}

func f() *A {
    return nil
}

func main() {
    var i interface{} = nil
    var j interface{} = f()
    if i == nil {
        fmt.Println("i is nil") // 表示される
    }
    if j == nil {
        fmt.Println("j is nil") // 表示されない (!)
    }
    if j.(*A) == nil {
        fmt.Println("j is nil") // 表示される
    }
    j = nil
    if j == nil {
        fmt.Println("j is nil") // 表示される
    }
}

Go言語では、nilは型の情報を含んでいるようです。そのため、(*A)型のnilinterface{}型のnilに変換するとおかしなことになります。

教訓

interface{}型のnilチェックでは気をつける

参考

https://qiita.com/umisama/items/e215d49138e949d7f805
https://stackoverflow.com/questions/19761393/why-does-go-have-typed-nil

プリミティブ型(chan)で変数の自動初期化を頼れない

Go言語では大概の変数は宣言すると同時に初期化されます。しかしながらchanの場合はどうでしょうか。

func main() {
    var ch chan int
    go func() {
        ch <- 123
    }()
    fmt.Println(<-ch) // fatal error: all goroutines are asleep - deadlock! と表示
}

解決法

make(chan int)を用いる

(2019.4.15追記)

chan型はnil-ableなので、宣言した直後はnilになっている、との事です。(下のコメント欄参照)

Go Modulesを使うために固有のURLを割り振る必要がある

Go1.11からGO Modulesが導入され、Go言語の標準機能でモジュールを簡単に扱えるようになりました。
Go Modulesを使用するためにはまず以下のようにgo mod initを実行する必要があります。

$ go mod init https://example.com/testproj

このプロジェクトに対してgo buildを実行すると、testprojという名前の実行ファイルが生成されます。
問題点は、公開していないプロジェクトでも、modulesを使用するためには何らかのURLを設定しなければならないことです。そういった場合どのようなURLを使えばいいのか、わかる方がいたらぜひ教えていただきたいです。

Goのツールチェーンに--verbose相当の機能がない

go言語のツールチェーンは、ビルドシステムを含んでいたり、モジュールを扱えるなど様々な機能を持っています。
ツールが様々な面倒を見てくれるのは便利である反面、おかしな挙動に出くわしたときに調べる手間が増えます。
その際、ツールの動作を調べるために--verboseオプションがあれば便利なのですが、現状用意されていません。よって、最悪Goツールチェインのソースコードまで戻って追う必要に迫られることがあります。

なぜかgotoが使える

Go言語ではgotoが使えます。(goだけに)
エラーハンドリングを一箇所にまとめるときに有用なようです。

参考: https://tmrtmhr.info/tech/why-does-golang-not-have-exceptions/

golang.org/x/text/messageでI18N

External article
Browsing Latest Articles All 25 Live