2017/10/26


printf デバッグは便利だ。技術の後退と言われようと printf でないと解決できない事はまだまだたくさんあります。

今日は net/http でクライアントが得たレスポンスの JSON を確認したいといった場合に、どうデバッグしたらいいかを書いてみたいと思う。

Go のインタフェースは大よそ io.Reader もしくは io.Writer を使う様に設計されている。こうする事でプログラムがメモリを一度に沢山確保してしまわない様にしています。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    var foo Foo
    err = json.NewDecoder(resp.Body).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

例えばこういうコードの、resp.Body に何が流れているのか確認したい場合、デバッグ出力する為に一旦 ioutil.ReadAll で全て読み取ったりしていないでしょうか。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    b, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(string(b))

    var foo Foo
    err = json.NewDecoder(bytes.NewReader(b)).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

デバッグ表示したいだけなのに、ちょっとコードが増えてしまった感じがしますよね。デバッグを無効にしたいときに消すコードも多い。しかも json.NewDecoder の部分にも手を入れてしまわないといけなくてなんだか嫌な感じもします。元のコードは json.NewDecoder の箇所に手を入れられるから良いですが、時には io.Reader を引数に持つ関数に渡す必要があったり、ioutil.ReadAll で全て読み取る事が出来ないストリームデータの場合には使えません。こういった場合は io.TeeReader を使います。

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

type Foo struct {
    ID  string `json:"id"`
    Content string `json:"content"`
}

func main() {
    resp, err := http.Get("http://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    var r io.Reader = resp.Body
    r = io.TeeReader(r, os.Stderr)

    var foo Foo
    err = json.NewDecoder(r).Decode(&foo)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(foo.Content)
}

こうしておき、必要に応じて r = io.TeeReader(r, os.Stderr) の行をコメントアウトすれば良いのです。コメントアウトを外せばデバッグ表示になります。メモリも節約出来てお得感ありますね。

Posted at by



2017/10/24


たぶん逆引きが無いから探せないのかなと思ったので path/filepath にどういう機能があるのか書いておく。

パスからファイル名を得る

filepath.Base を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Base("C:/foo/bar"))
}

この場合 bar が表示される。

パスからディレクトリ名を得る

filepath.Dir を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Dir(filepath.Clean(`../foo\bar`)))
}

この場合、..\foo が表示される。

パスからボリューム名を得る

filepath.VolumeName を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.VolumeName(`c:/windows/notepad.exe`))
}

この場合 c: が表示される。UNIX の場合は空文字列が返る。

相対パスから絶対パスに変換する

filepath.Abs を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    p, err := filepath.Abs("./testdata")
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

絶対パスから相対パスに変換する

filepath.Rel を使う。

package main

import (
    "log"
    "os"
    "path/filepath"
)

func main() {
    cwd, err := os.Getwd()
    if err != nil {
        log.Fatal(err)
    }
    p, err := filepath.Rel(cwd, `c:/dev`)
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

ディレクトリ配下であれば、それ以下の部分が。ディレクトリ配下でなければ .. で上昇した結果が返る。

パスを綺麗にする

../foo\bar\baz といった汚いパスを綺麗にするには filepath.Clean を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Clean(`../foo\bar`))
}

この場合、..\foo\bar が表示される。

シンボリックリンクのリンク元を得る

filepath.EvalSymlinks を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    p, err := filepath.EvalSymlinks(`c:/dirlink`)
    if err != nil {
        log.Fatal(err)
    }
    println(p)
}

Windows でも動作する。(ショートカットファイルではなくジャンクション)

パスから拡張子を得る

filepath.Ext を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Ext(`C:\Windows\Notepad.exe`))
}

.bashrc の様にドットで始まるファイル名を渡すと、ファイル名のまま返る。

スラッシュで区切られたパスを OS のパスセパレータに直す

filepath.FromSlash を使う。僕が path/filepath で一番好きな関数。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.FromSlash(`c:/users/mattn/.bashrc`))
}

この場合 c:\users\mattn\.bashrc が表示される。UNIX では何もしていない。Windows だけスラッシュがバックスラッシュに変換される。なので例えばファイルパスから URL のパスを作る時にこれを使ってくれると Windows ユーザが幸せになれる。

OS のパスセパレータで区切られたパスをスラッシュに直す

filepath.ToSlash を使う。filepath.FromSlash の逆。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.ToSlash(`c:\users\mattn\.bashrc`))
}

c:/users/mattn/.bashrc が表示される。

ファイルをマスクで検索する

filepath.Glob を使う。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    files, err := filepath.Glob(`c:\Windows\*`)
    if err != nil {
        log.Fatal(err)
    }
    for _, f := range files {
        println(f)
    }
}

Windows の場合、バックスラッシュはエスケープ文字として扱われない。また ** は使えない。使いたい場合は zglob を使う。

パスの先頭に特定のディレクトリが含まれるか確認する

filepath.HasPrefix を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.HasPrefix(`c:\Windows\Notepad.exe``c:\windows`))
}

気を付けないといけないのは、この動作は strings.HasPrefix でしかない事。この関数は deprecated として扱われている。

パスが絶対パスかを確認する

filepath.IsAbs を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.IsAbs(`..\Notepad.exe`))
}

パスを結合する

filepath.Join を使う。

package main

import (
    "path/filepath"
)

func main() {
    println(filepath.Join(`c:\windows``system32``drivers``etc``hosts`))
}

OS のパスセパレータで結合される。可変個引数なので複数渡せる。

パスをディレクトリ名とファイル名に分解する

filepath.Split を使う。

package main

import (
    "path/filepath"
)

func main() {
    dir, filename := filepath.Split(`c:\windows\notepad.exe`)
    println(dir, filename)
}

c:\windows\notepad.exe に分けられる。

パスリストを分解する

PATH 環境変数の様に OS のパスリストセパレータで結合された物を分解する。filepath.SplitList を使う。

package main

import (
    "os"
    "path/filepath"
)

func main() {
    for _, p := range filepath.SplitList(os.Getenv("PATH")) {
        println(p)
    }
}

パスがパターンにマッチするか確認する

filepath.Match を使う。Glob が中で使っている物に過ぎない。

package main

import (
    "log"
    "path/filepath"
)

func main() {
    ok, err := filepath.Match(`*.exe``c:/windows/notepad.exe`)
    if err != nil {
        log.Fatal(err)
    }
    println(ok)
}

ディレクトリを下ってファイルを探索する

filepath.Walk を使う。例えば特定パス配下のディレクトリだけを探すのであれば以下の様に実行する。

package main

import (
    "log"
    "os"
    "path/filepath"
)

func main() {
    root := `c:\windows\system32\drivers\`
    err := filepath.Walk(root, func(p string, info os.FileInfo, err errorerror {
        if info.IsDir()  {
            println(p)
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

探索を中断するには関数内でエラーを返す。もし特定のディレクトリ配下の探索をやめたいのであれば filepath.SkipDir を return で返してあげる。

なお物理ファイルの操作に path/filepath ではなく path を使うと爆発します。ちなみに、なぜここまで口をすっぱく言っているのかと言うと、UNIX で実装した物を Windows に持ってくると動かないからです。それどころかセキュリティ issue にもなり得る。

package main

import (
    "io"
    "net/http"
    "os"
    "path"
)

func main() {
    cwd, _ := os.Getwd()

    http.HandleFunc("/"func(w http.ResponseWriter, r *http.Request) {
        if ok, err := path.Match("/download/*", r.URL.Path); err != nil || !ok {
            http.NotFound(w, r)
            return
        }
        name := path.Join(cwd, r.URL.Path)
        f, err := os.Open(name)
        if err != nil {
            http.NotFound(w, r)
            return
        }
        defer f.Close()
        io.Copy(w, f)
    })
    http.ListenAndServe(":8080"nil)
}

何かをダウンロードさせるのにこういったコードを書いてしまうと、以下の様なリクエストでディレクトリトラバーサルが発生する。(正しくは http.ServeFile を使ってね)

http://localhost:8080/download/..%5cmain.go

これは Go のライブラリが悪い訳じゃない。こんなコードを書いた人が悪い。この辺は「みんなの Go 言語」にも書かれている。

みんなのGo言語[現場で使える実践テクニック] みんなのGo言語[現場で使える実践テクニック]
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧大輔, 鈴木健太
技術評論社 Kindle版 / ¥2,178 (2016年09月09日)
 
発送可能時間:

Posted at by



2017/08/25


gRPC は型の強い RPC を色々な言語を使って実装できる仕組みとライブラリです。

Big Sky :: Protocol Buffers を利用した RPC、gRPC を golang から試してみた。

grpc/grpc · GitHub gRPC - An RPC library and framework https://github.com/grpc/grpc gRPC は Google が開...

https://mattn.kaoriya.net/software/lang/go/20150227144125.htm

とても便利なのですが幾分手数が多いのが難点で、ちょっとしたサービスを gRPC で実装したいと思っていてもそう簡単に作る事が出来ませんでした。

ところが今回ご紹介する lile を使うと、とても簡単に gRPC を使った golang の実装を作れてしまいます。

GitHub - lileio/lile: Easily create gRPC services in Go

readme.md ALPHA: Lile is currently considered "Alpha" in that things may change. Currently I am gath...

https://github.com/lileio/lile

lile は gRPC のスケルトンを生成するコマンドとライブラリセットです。今日はこれを使って簡単に gRPC のサービスを作ってみます。お題は GENE95 辞書 を gRPC 経由で照会するサービスです。

まず lile をインストールするには以下のコマンドを実行します。

$ go get github.com/lileio/lile/...

Windows の人は今 pull-request を作ってるのでそちらを使って下さい。執筆時点でまだマージされてませんが。

lile をインストールしたらまずスケルトンを生成します。

$ lile new gene9go
Creating project in /home/mattn/go/src/github.com/mattn/lile-example/gene9go
Is this OK? [y]es/[n]o
yes
.
├── server
│   ├── server.go
│   └── server_test.go
├── subscribers
│   └── subscribers.go
├── gene9go
│   ├── cmd
│       ├── root.go
│       ├── serve.go
│       ├── subscribe.go
│       └── up.go
│   └── main.go
├── gene9go.proto
├── Makefile
├── Dockerfile
├── .travis.yml
└── .gitignore

git push できるくらいの物が生成されています。次に proto ファイルを編集して独自のインタフェースを作成します。元の proto ファイルは以下の様になっています。

syntax = "proto3";
option go_package = "github.com/mattn/lile-example/gene9go";
package gene9go;

message Request {
  string id = 1;
}

message Response {
  string id = 1;
}

service Gene9go {
  rpc Read (Request) returns (Response) {}
}

これを以下の様に編集しました。

syntax = "proto3";
option go_package = "github.com/mattn/lile-example/gene9go";
package gene9go;

message Request {
  string Word = 1;
}

message Response {
  string Text = 1;
}

service Gene9go {
  rpc Translate (Request) returns (Response) {}
}

辞書引きなので Translate メソッドを追加しています。編集し終えたら Makefile のある場所で make を実行します。するとこの proto ファイルから Translate メソッドのスケルトンが生成されます。

package server

import (
    "errors"

    "github.com/mattn/lile-example/gene9go"
    context "golang.org/x/net/context"
)

func (s Gene9goServer) Translate(ctx context.Context, r *gene9go.Request) (*gene9go.Response, error) {
    return nil, errors.New("not yet implemented")
}
尚、この時点でテストコードのスタブも生成されます。とても便利です。

package server

import (
    "testing"

    "github.com/mattn/lile-example/gene9go"
    "github.com/stretchr/testify/assert"
    context "golang.org/x/net/context"
)

func TestTranslate(t *testing.T) {
    ctx := context.Background()
    req := &gene9go.Request{}

    res, err := cli.Translate(ctx, req)
    assert.Nil(t, err)
    assert.NotNil(t, res)
}

さてでは Translate メソッドの中身を実装します。gene.txt ファイルを読み込んで単語にマッチする次の行を返しているだけです。gene.txt は utf-8 で保存しておいて下さい。

package server

import (
    "bufio"
    "os"
    "strings"

    "github.com/mattn/lile-example/gene9go"
    context "golang.org/x/net/context"
)

func translate(word string) (stringerror) {
    f, err := os.Open("gene.txt")
    if err != nil {
        return "", err
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    found := ""
    for scanner.Scan() {
        first := scanner.Text()
        if !scanner.Scan() {
            break
        }

        if strings.ToLower(first) == strings.ToLower(word) {
            found = scanner.Text()
            break
        }
    }
    return found, scanner.Err()
}

func (s Gene9goServer) Translate(ctx context.Context, r *gene9go.Request) (*gene9go.Response, error) {
    text, err := translate(r.GetWord())
    if err != nil {
        return nil, err
    }
    return &gene9go.Response{Text: text}, nil
}

あとはクライアントとサーバを作ります。といっても Register 関数が用意されているのでこちらも簡単。まずはサーバ。

package main

import (
    "log"
    "net"

    "github.com/mattn/lile-example/gene9go"
    "github.com/mattn/lile-example/gene9go/server"
    "google.golang.org/grpc"
)

func main() {
    lis, err := net.Listen("tcp"":11111")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    srv := grpc.NewServer()

    gene9go.RegisterGene9goServer(srv, &server.Gene9goServer{})
    srv.Serve(lis)
}
そしてクライアント。
package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mattn/lile-example/gene9go"
    "google.golang.org/grpc"
)

func main() {
    conn, err := grpc.Dial("localhost:11111", grpc.WithInsecure())
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    client := gene9go.NewGene9goClient(conn)
    req := &gene9go.Request{
        Word: os.Args[1],
    }
    resp, err := client.Translate(context.Background(), req)
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(resp.Text)
}
後から気付いたのですが、gene9go の下にサーバコマンドも生成されてました。いたれりつくせり過ぎる。
A gRPC based service

Usage:
  gene9go [command]

Available Commands:
  help        Help about any command
  serve       Run the RPC server
  subscribe   Subscribe to and process queue messages
  up          up runs both RPC and pubub subscribers

Flags:
  -h, --help   help for gene9go

Use "gene9go [command] --help" for more information about a command.

これだけです。なにこれ超簡単じゃん。サーバを起動した状態で、クライアントに単語を付けて起動します。

$ client Go
1.〜に進行する,行く,をしに行く,動く,過ぎる,至る,及ぶ,2.〜と書いてある

これを実装するのにわずか20分程度しか掛かりませんでした。今まで gRPC を使ったサービスに興味があったけど実装難しいと思っていた方はぜひ lile を使ってみて下さい。あっと言う間に実装できるはずです。

この記事で作ったソースファイルは以下に置いておきます。

GitHub - mattn/lile-example
https://github.com/mattn/lile-example
Posted at by