20190526のGoに関する記事は7件です。

Golang の文字列内で変数を展開する方法(各種)

Go で文字列中に変数を使いたい

以下と同じことを Go 言語でもしたい。

  • bash でいう hoge="fuga ${piyo:-piyopiyo} mogera"
  • php でいう $hoge = "fuga ${piyo} mogera";
  • ruby でいう hoge = "fuga #{piyo} mogera"
  • python でいう hoge = f'fuga {piyo} mogera' (python >= 3.6 に限る)

TS;DR

Golang には interpolation(補間、内挿)機能はありません。つまり、文字列内で変数をパース(展開)する機能は標準では持っていません。コンパイル型の言語なので工夫が必要です。

  1. python 3.6 より前のように、変数と文字列を連結演算子(+)でつなげる。
  2. 文字列フォーマット関数 Sprintf を使う。
  3. 大量に置き換える場合は、texthtml ライブラリの template 関数を使う。
  4. または、bash${piyo:-piyopiyo} のようにデフォルトも設定したい場合は、外部ライブラリをインストールするなどの方法を検討する。

TL;DR

+演算子で結合するタイプ
func GetHogeByGlue(piyo string) string {
    return "fuga " + piyo + " mogera" + "\n"
}
Sprintf関数を使うタイプ
// 要 import "fmt"
func GetHogeBySprintf(piyo string) string {
    return fmt.Sprintf("fuga %s mogera\n", piyo)
}
Template関数を使うタイプ
// 要 import "text/template"

type FieldsToReplace struct {
    Replace1 string
}

func GetHogeByTemplate(piyo string) string {
    var msg_result bytes.Buffer

    msg_tpl, msg_err := template.New("myTemplate").Parse("fuga {{.Replace1}} mogera\n")    
    replace_to := FieldsToReplace {
        Replace1: piyo,
    }
    msg_err = msg_tpl.Execute(&msg_result, replace_to)

    return msg_result.String()
}

外部ライブラリを使った例

bash などの POSIX Parameter Expansion 形式で使いたい場合は github.com/buildkite/interpolate ライブラリが便利です。

interpolateパッケージを使う
// 要インストール go get -u github.com/buildkite/interpolate
// 要 import "github.com/buildkite/interpolate"

func GetHogeByInterpolate(piyo string) string {

    env := interpolate.NewSliceEnv([]string{
        "Replace2="+piyo,
    })

    output, _ := interpolate.Interpolate(env, "fuga ${Replace2} mogera ${Replace3:-?}\n")

    return output
}

上記すべての手法を1つにまとめたサンプル

sample.go
package main

import (
    "fmt"
    "text/template"
    "bytes"
    "github.com/buildkite/interpolate"
)

func main(){
    print(GetHogeByGlue("foo"))
    print(GetHogeBySprintf("foo"))
    print(GetHogeByTemplate("foo"))
    print(GetHogeByInterpolate("foo"))
}

func GetHogeByGlue(piyo string) string {
    return "fuga " + piyo + " mogera" + "\n"
}

func GetHogeBySprintf(piyo string) string {
    return fmt.Sprintf("fuga %s mogera\n", piyo)
}

type FieldsToReplace struct {
    Replace1 string
}

func GetHogeByTemplate(piyo string) string {

    msg_tpl, msg_err := template.New("myTemplate").Parse("fuga {{.Replace1}} mogera\n")

    replace_to := FieldsToReplace{
        Replace1: piyo,
    }

    var msg_result bytes.Buffer

    msg_err = msg_tpl.Execute(&msg_result, replace_to)

    if msg_err != nil {
        fmt.Println(msg_err)
    }

    return msg_result.String()
}

func GetHogeByInterpolate(piyo string) string {

    env := interpolate.NewSliceEnv([]string{
        "Replace2="+piyo,
    })

    output, _ := interpolate.Interpolate(env, "fuga ${Replace2} mogera ${Replace3:-?}\n")

    return output
}
$ go version
go version go1.12.5 linux/amd64
$ go get -u github.com/buildkite/interpolate
...
$ ls
sample.go
$ go run sample.go
fuga foo mogera
fuga foo mogera
fuga foo mogera
fuga foo mogera ?

Golang に文字列内変数置き換えがない理由

If you see the example from PHP or Ruby, you might realize that the they are more for dynamic type system because it does care about what you pass into the interpolation string — the data types of year, month and day, however in a static, compile language like Go, we need to specify types of our variables.
Conclusion
It might looks weird to you, but if you think about the benefit of static type language with compile time error checking, this make a lot of sense, more convenient and easy to debug than string interpolation in dynamic languages.
(「String interpolation in golang」@ Medium より)

PHP や Ruby の例を見ると、動的型システムに適していることがわかると思います。なぜなら文字列内に置き換えるために渡すものを重要視するからです。しかし、静的型システム(Go のようなコンパイル言語)の場合は年/月/日といったデータの型の変数は事前に明確にする必要があるのです。
結論
奇妙に見えるかもしれませんが、コンパイル時のエラーチェックによる静的型言語の利点を考えると、動的言語での文字列補間よりもはるかに理にかなっていて、デバッグがより便利で簡単です。
(筆者訳)

なるほど。でも、まぁ PHP7 や Python3 も、いまやコンパイル型に近い(プリ・コンパイル型)言語。PHP7 だと Golang より速いことも多いので、バイナリで配布できるのが一番のメリットかもしれないけど、コンパイルうんぬんと言うよりポリシーの問題かと。

確かに文字列内の変数の展開は Web 用途に多いので text/templatehtml/template を使うべし、というのは理解できる。でも、やはり、Python3.6 でそうなったように、Golang も変数の文字列内展開を標準にして欲しいなぁ。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

内部実装から理解するgRPC

概要

目的

gRPCはDocumentにあるように以下の特徴があるかと思います。

  • protocol buffer のようなインターフェース定義語 (IDL) から生成されたコードを利用してRPCができる
  • HTTP/2で通信することができ、リクエストとレスポンスをそれぞれ分割できる
  • 多言語に対応している

しかし、この記事ではこれらの機能の紹介ではなく、gRPCの仕組みを理解することを意図しています。
なぜそれを意図したかというと普段の開発でgRPCを利用しているものの、どのような仕組みでRPCが実現できているのかイメージが持てていなかったためです。そのために、grpc-goの内部実装(2019/5時点)を読み解きながら、実際の通信の中身を覗いてみました。

そして結果的には以下の効用がありました。

  • protoc-gen-goがprotocol-buffersから生成したコードがどのように利用されているか分かる
  • HTTP/2により多重化されたリクエストをどう扱っているか分かる
  • Webサーバーを実装をする時にどのような設計にすればいいのか参考になる

処理流れ

まずソースコードを読むとサーバー側は以下のようなフローで処理されていることが分かります。

grpc.png

goroutineが三重に実行されています。具体的には以下の順番で処理が進みます。

  1. TCP Connection の受付
  2. TLSハンドシェイクを並行処理
  3. フレームの読み取りを並行処理
  4. gRPC method の実行を並行処理

1は2の完了を監視しており、func (*WaitGroup) Addを2の呼び出し前で実行し、2が完了する前にfunc (*WaitGroup) Doneを実行しています。

この二つの処理でTCPやTLSの通信の確立を担っており、それ以降の処理を別のgoroutineに委ねています。こうすることで、後続の通信の確立を3, 4の処理でブロックしてしまわないようにしています。

そして、次の3, 4が実際のHTTP/2の扱いとgRPC methodの呼び出しを行います。3ではリクエストからフレームを読み取り、gRPC methodに対応したchannelにデータを格納します。

HTTP/2ではリクエスト(RPCでは一つのメソッド呼び出し)は一つの通信に多重化されるので、複数のgRPC methodの呼び出しがされている可能性もあります。そこで、それぞれのgRPC methodごとにgoroutineを起動し、channelからデータを取得し、IDLで定義したリクエストのフォーマットに従った構造体にUnmarshalし、gRPC methodの呼び出しを行います。これらが大きな処理の流れになります。

HTTP/2に関する前提知識

実際に詳しくソースコードを理解するためにはHTTP/2に関する知識が不可欠です。HTTP/2はHTTP/1.1の抱えるパフォーマンス上の課題を解決することを主な目的として開発されました。

そのパフォーマンス強化の中心となる仕組みとして、バイナリフォーマットでHTTPのメソッド、ヘッダ、ボディなどの構成要素がHTTPのレイヤーにおいて表現されること(バイナリフレーミング)があります。HTTP/1.xまでは改行で区切られたプレーンテキストでこれらを表現していました。リクエストは複数の改行されたヘッダフィールドを持ち、レスポンスの最初の行はステータスコードであるといった具合です。

そのため、一つの通信に一つのリクエスト/レスポンスという関係でした。しかしHTTP/2ではリクエスト/レスポンスをHTTPレベルでバイナリとして扱えるので、それらを分割し、一つの通信において多重化し、受信側で再構成をすることができます。gRPCの実装においてもこれらがなされています。

binary_framing.png
こちらを参照

まず、これらのプロセスを理解するためにHTTP/2に関する用語を確認します。

  • ストリーム
    • 確立した接続内の双方向の論理的なフレームの流れのまとまりを表現したもの
  • メッセージ
    • 論理的なリクエストまたはレスポンス、メッセージを表現するためにフレームを順番にまとめたもの
  • フレーム
    • HTTP/2における通信の最小単位で、それぞれのフレームはヘッダを持ち、ヘッダはそのフレームが所属するストリームを識別する

stream_message_frame.png
こちらを参照

メッセージとフレームの関係は従来のHTTP/1.xからイメージしやすいかと思いますが、ストリームという概念がなぜ存在するのかが私には分かりづらかったです。調べてみると、ストリームは以下の目的のために存在しているようです。

  • フレームの流れのライフサイクルをフレームのタイプによって制御する
  • フレームの流れごとに優先順位を付ける
  • フレームの流れごとにフロー制御する

フレームの流れのライフサイクルをフレームタイプによって制御する

ストリームごとに状態を持ちます。RFCに状態遷移図があります。
実際に通信されるのはフレームなので、フレームの持つタイプに応じて、ストリームの開始、ストリームにおけるフレームの扱い、ストリームの終了が遷移します。

フレームの流れごとに優先順位を付ける

https://summerwind.jp/docs/rfc7540/#section5-3
HTTPメッセージが複数のフレームに分割されると、一つの通信で複数のストリームに所属するフレームが送信されることになります。
ストリームを開始するHEADERSフレームに優先順位を持たせることで、複数のストリームにおけるリソース割り当ての方法を明示することができます。

フレームの流れごとにフロー制御する

https://summerwind.jp/docs/rfc7540/#section6-9
TCPでは送信受信共にバッファにデータを格納し互いに通信するので、それを上回らないように利用状況を交換、調整します。
HTTP/2では、複数のストリームを同じTCP接続上で多重化するので、リソース割り当てをHTTPレベルでも管理する必要があります。
なぜならTCPのフロー制御では、単一の接続内に存在する複数のストリームを判別できないからです。これをWINDOW_UPDATEフレームにより実現します。

gRPCを実際に呼び出す

それでは、実際にgRPCを使ってクライアントとサーバーで通信してみます。
ここの実装はgrpc-go/examplesを利用しています。

Protocol Buffer から go のソースコードを生成する

まず、以下のようにprotocol bufferの定義を定義します。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.proto

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

そして、protocol buffer の定義に沿ったgRPC用の go のソースコードを生成します。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.pb.go

サーバーサイドの実装

以下のようにgRPC method を実装し、gRPCサーバーを起動します。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
    log.Printf("Received: %v", in.Name)
    return &pb.HelloReply{Message: "Hello " + in.Name}, nil
}

func main() {
    lis, err := net.Listen("tcp", port)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve: %v", err)
    }
}

ここでは、実装したgRPC methodをgrpc.Server構造体に格納されます。
これはこの後のgRPCのメソッド呼び出しに応じて、gRPC Service名、method名に応じて取り出され、実行されることになります。

// server is used to implement helloworld.GreeterServer.
type server struct{}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
  ...
}

func main() {
  ...
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
  ...
}

内部的には、protocol buffer から生成された以下のgrpc.ServiceDes構造体をgrpc.Server構造体に登録しています。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.pb.go#L148

func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
    s.RegisterService(&_Greeter_serviceDesc, srv)
}

var _Greeter_serviceDesc = grpc.ServiceDesc{
    ServiceName: "helloworld.Greeter",
    HandlerType: (*GreeterServer)(nil),
    Methods: []grpc.MethodDesc{
        {
            MethodName: "SayHello",
            Handler:    _Greeter_SayHello_Handler,
        },
    },
    Streams:  []grpc.StreamDesc{},
    Metadata: "helloworld.proto",
}

上記のMethodDesc構造体のMethodNameフィールドをkey, MethodDesc構造体自体をvalueとして、grpc.service構造体のmdフィールドに格納していることが分かります。
そして、このgrpc.service構造体はgrpc.Server構造体のmフィールドに格納されます。
https://github.com/grpc/grpc-go/blob/master/server.go#L426

func (s *Server) RegisterService(sd *ServiceDesc, ss interface{}) {
  ...
    s.register(sd, ss)
}

func (s *Server) register(sd *ServiceDesc, ss interface{}) {
        ...
    srv := &service{
        server: ss,
        md:     make(map[string]*MethodDesc),
        sd:     make(map[string]*StreamDesc),
        mdata:  sd.Metadata,
    }
    for i := range sd.Methods {
        d := &sd.Methods[i]
        srv.md[d.MethodName] = d
    }
        ...
    s.m[sd.ServiceName] = srv
}

https://github.com/grpc/grpc-go/blob/master/server.go#L80

// service consists of the information of the server serving this service and
// the methods in this service.
type service struct {
        ...
    md     map[string]*MethodDesc
}

// Server is a gRPC server to serve RPC requests.
type Server struct {
        ...
    m      map[string]*service // service name -> service info
        ...
}

要するに以下のような構造体にそれぞれ作られることになります。

register.png

そのようにgRPC methodが登録されたServerを起動します。

func main() {
    lis, err := net.Listen("tcp", port)
        ...
    s := grpc.NewServer()
    pb.RegisterGreeterServer(s, &server{})
    s.Serve(lis)
        ...
}

クライアントからの呼び出し

今度は、以下のようにprotocol-buffersから生成したgoのクライアントサイドの実装を使って、gRPCサーバーにリクエストします。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go

func main() {
    // Set up a connection to the server.
    conn, err := grpc.Dial(address, grpc.WithInsecure())
        ...
    c := pb.NewGreeterClient(conn)
        ...
    r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
        ...
}

これは、内部的には新規にストリームを作成し、gRPCのメソッド呼び出しをしてレスポンスを待っています。
https://github.com/grpc/grpc-go/blob/master/examples/helloworld/helloworld/helloworld.pb.go#L133

func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
    out := new(HelloReply)
    err := c.cc.Invoke(ctx, "/helloworld.Greeter/SayHello", in, out, opts...)
        ...
    return out, nil
}

https://github.com/grpc/grpc-go/blob/master/call.go#L65

func invoke(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, opts ...CallOption) error {
    cs, err := newClientStream(ctx, unaryStreamDesc, cc, method, opts...)
    if err != nil {
        return err
    }
    if err := cs.SendMsg(req); err != nil {
        return err
    }
    return cs.RecvMsg(reply)
}

実際にwiresharkを使って、この通信をみてみます。

request.png

複数のフレームが送受信されていることが確認できます。そして全てのフレームはStreamIDが1になっており、一つのストリームで通信されています。

gRPC Server 側の挙動を確認する

今度はメソッド呼び出しされたgRPCサーバー側の挙動を確認します。

grpc.png

概要で説明したように以下の流れでgRPC method の呼び出しが行われます。

  1. TCP Connection の確立
  2. TLSハンドシェイクを並行処理
  3. Streamの読み取りを並行処理
  4. gRPC method の実行を並行処理

TCP/TLSのコネクション確立

先ほどは、以下のようにServer.Serveメソッドを呼び出しました。

func main() {
    lis, err := net.Listen("tcp", port)
        ...
    s.Serve(lis)
        ...
}

そこでは、TCPコネクションが確立されると、net.Connインターフェースを返し、Server.handleRawConnにそれを渡してgoroutineを実行します。
https://github.com/grpc/grpc-go/blob/master/server.go#L545

func (s *Server) Serve(lis net.Listener) error {
        ...
    for {
        rawConn, err := lis.Accept()
                ...
        go func() {
            s.handleRawConn(rawConn)
            s.serveWG.Done()
        }()
    }
}

そして、Server.handleRawConnでは以下の二つを行います。

  • TLSハンドシェイクによる通信の確立
  • net.Connからtransport.ServerTransportインターフェースを作成

具体的には以下のように、まずnet.ConnからTLSハンドシェイクをし、次に受け取ったTCPコネクションの情報からtransport.ServerTransportインターフェースを作成して、Server.serveStreamsに渡します。
https://github.com/grpc/grpc-go/blob/master/server.go#L639

func (s *Server) handleRawConn(rawConn net.Conn) {
        ...
   conn, authInfo, err := s.useTransportAuthenticator(rawConn)
        ...
    // Finish handshaking (HTTP2)
    st := s.newHTTP2Transport(conn, authInfo)
        ...
    go func() {
        s.serveStreams(st)
        s.removeConn(st)
    }()
}

このnewHTTP2Transporで作成されているtransport.ServerTransportインターフェースはgRPCのサーバーサイドがストリームを扱うための実装になっています。
https://github.com/grpc/grpc-go/blob/master/internal/transport/transport.go#L631

// ServerTransport is the common interface for all gRPC server-side transport
// implementations.
//
// Methods may be called concurrently from multiple goroutines, but
// Write methods for a given Stream will be called serially.
type ServerTransport interface {
    // HandleStreams receives incoming streams using the given handler.
    HandleStreams(func(*Stream), func(context.Context, string) context.Context)

    // WriteHeader sends the header metadata for the given stream.
    // WriteHeader may not be called on all streams.
    WriteHeader(s *Stream, md metadata.MD) error

    // Write sends the data for the given stream.
    // Write may not be called on all streams.
    Write(s *Stream, hdr []byte, data []byte, opts *Options) error
  ...
}

実際には、transport.ServerTransportインターフェースを満たすtransport.http2Server構造体を作成して返しています。
https://github.com/grpc/grpc-go/blob/master/server.go#L682
https://github.com/grpc/grpc-go/blob/master/internal/transport/transport.go#L488
https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_server.go#L126

// newHTTP2Server constructs a ServerTransport based on HTTP2. ConnectionError is
// returned if something goes wrong.
func newHTTP2Server(conn net.Conn, config *ServerConfig) (_ ServerTransport, err error) {
  ...
    framer := newFramer(conn, writeBufSize, readBufSize, maxHeaderListSize)
  ...
    t := &http2Server{
    ...
        conn:              conn,
        framer:            framer,
        readerDone:        make(chan struct{}),
        activeStreams:     make(map[uint32]*Stream),
        stats:             config.StatsHandler,
        initialWindowSize: iwz,
    }
  ...
    return t, nil
}

ここのフィールドをみるとフレーム扱うためのframer構造体へのポインタや、現在の接続下におけるストリームなどHTTP/2を扱うための情報が格納されていることが分かります。
そして、その構造体をServer.serveStreamsに渡してgoroutineを起動します。

func (s *Server) handleRawConn(rawConn net.Conn) {
  ...
    st := s.newHTTP2Transport(conn, authInfo)
  ...
    go func() {
        s.serveStreams(st)
        s.removeConn(st)
    }()
}

ストリームの読み取り

呼び出されたServer.serveStreamsでは、先ほど渡されたtransport.ServerTransportインターフェースのHandleStreamsを実行します。
第一引数には、Server.handleStreamを実行する関数が渡されています。これは実際にStreamから読み取ったデータを使ってgRPC method を実行する役割を担っています。(概要図参照)
https://github.com/grpc/grpc-go/blob/master/server.go#L713

func (s *Server) serveStreams(st transport.ServerTransport) {
  ...
  // Streamの読み取りをし、goroutineでServer.handleStreamを実行する
    st.HandleStreams(func(stream *transport.Stream) {
        wg.Add(1)
        go func() {
            defer wg.Done()
      // Streamの読み取り結果に応じた gRPC method の呼び出しをする
            s.handleStream(st, stream, s.traceInfo(st, stream))
        }()
    },
  ...)
  ...
}

transport.ServerTransportインターフェースは上で確認したように、実際はtransport.http2Server構造体なので、以下が実行されます。
https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_server.go#L428

// HandleStreams receives incoming streams using the given handler. This is
// typically run in a separate goroutine.
// traceCtx attaches trace to ctx and returns the new context.
func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context.Context, string) context.Context) {
  ...
    for {
        frame, err := t.framer.fr.ReadFrame()
        ...
        if err != nil {
            if err == io.EOF || err == io.ErrUnexpectedEOF {
                t.Close()
                return
            }
                ...
            return
        }
        switch frame := frame.(type) {
        case *http2.MetaHeadersFrame:
            if t.operateHeaders(frame, handle, traceCtx) {
                t.Close()
                break
            }
        case *http2.DataFrame:
            t.handleData(frame)
        case *http2.RSTStreamFrame:
            t.handleRSTStream(frame)
        case *http2.SettingsFrame:
            t.handleSettings(frame)
        case *http2.PingFrame:
            t.handlePing(frame)
        case *http2.WindowUpdateFrame:
            t.handleWindowUpdate(frame)
        case *http2.GoAwayFrame:
            // TODO: Handle GoAway from the client appropriately.
        default:
            errorf("transport: http2Server.HandleStreams found unhandled frame type %v.", frame)
        }
    }
}

ここでは、http2パッケージのFramer.ReadFrameメソッドを実行してHTTP/2のフレームを順次読み取りフレームタイプに応じて処理を分岐させています。フレームタイプとは、ストリームの状態を管理するためのもので、現在や残りのフレームをどのように解釈されるかを表します。
https://summerwind.jp/docs/rfc7540/#section6

まず、クライアントが新規のストリームを開始すると、HEADERSフレームが送信されます。
このHEADERSフレームはヘッダに新規ストリームのIDを持ち、ペイロードにHTTPヘッダのKeyValueのペアを持ちます。

request_headers.png

そして、そのフレームを受け取ると、http2Server.operateHeadersが実行されます。
そこでは、フレームから所属するストリームのIDを取得してtransport.Stream構造体を作成し、それを引数で受け取る関数に渡して実行しています。
さらにtransport.newRecevBufferによりフレームから読み取ったデータを受け渡しするChannel作成し、構造体に登録しています。
https://github.com/grpc/grpc-go/blob/master/internal/transport/transport.go#L188
https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_server.go#L287
https://github.com/grpc/grpc-go/blob/master/internal/transport/transport.go#L64

// operateHeader takes action on the decoded headers.
func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func(*Stream), traceCtx func(context.Context, string) context.Context) (fatal bool) {
    streamID := frame.Header().StreamID
  ...
  // Channelの作成
    buf := newRecvBuffer()
  // 新規ストリームを表現する構造体の作成
    s := &Stream{
        id:             streamID,
        st:             t,
        buf:            buf,
        fc:             &inFlow{limit: uint32(t.initialWindowSize)},
        recvCompress:   state.data.encoding,
        method:         state.data.method,
        contentSubtype: state.data.contentSubtype,
    }
  ...
    s.trReader = &transportReader{
        reader: &recvBufferReader{
            ctx:     s.ctx,
            ctxDone: s.ctxDone,
            recv:    s.buf,
        },
        windowHandler: func(n int) {
            t.updateWindow(s, uint32(n))
        },
    }
  ...
  // 引数の関数に作成したtransport.Stream構造体を渡す
    handle(s)
    return false
}

HTTP/2では新規ストリームがHEADERSフレームにより作成されると、ペイロードにアプリケーションデータを格納しDATAフレームが送信されます。

request_data.png

フレームタイプがDATA(http2.DataFrame)の場合は、http2Server.handleDataが実行されます。
ここでは、フレームからtransport.Stream構造体を取得して、そこに登録されたChannelに対して読み取ったフレームのデータを書き込んでいます。
https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_server.go#L544

func (t *http2Server) handleData(f *http2.DataFrame) {
  ...
    // Select the right stream to dispatch.
    s, ok := t.getStream(f)
  ...
    if size > 0 {
    ...
        // TODO(bradfitz, zhaoq): A copy is required here because there is no
        // guarantee f.Data() is consumed before the arrival of next frame.
        // Can this copy be eliminated?
        if len(f.Data()) > 0 {
            data := make([]byte, len(f.Data()))
            copy(data, f.Data())
            s.write(recvMsg{data: data})
        }
    }
    if f.Header().Flags.Has(http2.FlagDataEndStream) {
        // Received the end of stream from the client.
        s.compareAndSwapState(streamActive, streamReadDone)
        s.write(recvMsg{err: io.EOF})
    }
}

内部的にはバッファありChannelにrecvMsg構造体を送信しています。
https://github.com/grpc/grpc-go/blob/master/internal/transport/transport.go#L71

func (s *Stream) write(m recvMsg) {
    s.buf.put(m)
}

type recvBuffer struct {
    c       chan recvMsg
    mu      sync.Mutex
    backlog []recvMsg
    err     error
}

func (b *recvBuffer) put(r recvMsg) {
    b.mu.Lock()
  ...
    if len(b.backlog) == 0 {
        select {
        case b.c <- r:
            b.mu.Unlock()
            return
        default:
        }
    }
    b.backlog = append(b.backlog, r)
    b.mu.Unlock()
}

読み取ったストリームからgRPC methodの実行

呼び出し元の実装を再び確認すると、このtrasport.Stream構造体を受け取る関数の実行によりServer.handleStreamメソッドが実行されてることが分かります。
https://github.com/grpc/grpc-go/blob/master/server.go#L713

func (s *Server) serveStreams(st transport.ServerTransport) {
  ...
    st.HandleStreams(func(stream *transport.Stream) {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s.handleStream(st, stream, s.traceInfo(st, stream))
        }()
    },
  ...)
  ...
}

このServer.handleStreamメソッドは先ほど確認した、HEADERSフレームを読み取ってからtrasport.Stream構造体を引数に呼び出されます。
HEADERSフレームのペイロードにはHTTPヘッダの情報が格納されており、その一つであるヘッダキーpathには、Service名とMethod名がセットされています。

request_only_headers.png

それをstream.Method()で取り出しているので以下のような結果が返ってきます。

/helloworld.Greeter/SayHello

次にそれを使って、gRPCサーバー起動前にgrpc.Serverのmフィールドに登録したgrpc.service構造体を取り出します。
これは最初に説明したように、Service名とgrpc.MethodDesc構造体を対応付けており、さらにmethod名から対応するgRPC methodを取得できます。

register.png

そして、Server.processUnaryRPCメソッドを呼び出します。
https://github.com/grpc/grpc-go/blob/master/server.go#L1248

func (s *Server) handleStream(t transport.ServerTransport, stream *transport.Stream, trInfo *traceInfo) {
  // pathヘッダの値を取得 e.g /helloworld.Greeter/SayHello
    sm := stream.Method()
  ...
  pos := strings.LastIndex(sm, "/")
  ...
    service := sm[:pos]
    method := sm[pos+1:]

  // Service名からgrpc.service構造体を取得
    srv, knownService := s.m[service]
    if knownService {
    // Method名からgrpc methodを取得
        if md, ok := srv.md[method]; ok {
            s.processUnaryRPC(t, stream, srv, md, trInfo)
            return
        }
    ...
    }
  ...
}

Server.processUnaryRPCメソッドでは主に以下の四つを実行しています。

  • ストリームを読み取る
  • そのバイト列をリクエストの構造体にUnmarshalする
  • grpc.MethodDesc構造体に登録されたgRPC methodを実行する
  • 実行結果を元にレスポンスをバイト列に書き込む

https://github.com/grpc/grpc-go/blob/master/server.go#L859

func (s *Server) processUnaryRPC(t transport.ServerTransport, stream *transport.Stream, srv *service, md *MethodDesc, trInfo *traceInfo) (err error) {
  ...
  // Channel経由でストリームのデータを読み取る
    d, err := recvAndDecompress(&parser{r: stream}, stream, dc, s.opts.maxReceiveMessageSize, payInfo, decomp)
  ...
    df := func(v interface{}) error {
    // protocol buffer から生成されたリクエストの構造体に読み取ったデータをUnmarshalする
        if err := s.getCodec(stream.ContentSubtype()).Unmarshal(d, v); err != nil {
            return status.Errorf(codes.Internal, "grpc: error unmarshalling request: %v", err)
        }
    ...
        return nil
    }
  ...
  // 登録されていたgRPC methodを読み出す
    reply, appErr := md.Handler(srv.server, ctx, df, s.opts.unaryInt)
  ...
    opts := &transport.Options{Last: true}

  // その実行結果をレスポンスに書き込む
    if err := s.sendResponse(t, stream, reply, cp, opts, comp); err != nil {
    ...
    }
  ...
    err = t.WriteStatus(stream, status.New(codes.OK, ""))
  ...
    return err
}

これで、gRPCサーバー側の主な処理を確認することができました!

TCPコネクションはいつ終了するのか

HTTP/2は1オリジンに1つのTCPコネクションを持ちます。これによって、ストリームをどれだけ並列化しても複数のTCPコネクションが不要になります。
そのため初回のストリームの作成時にのみTCPの通信に関するオーバーヘッドがかかるようになり、スループットが改善しました。

しかし、TCPコネクションがいつ終了するのかは、RFCを読んでもあまり言及されていませんでした。
少なくとも、GOAWAYフレームが送信された時に新規のストリームの作成を止めて、TCPコネクションを終了させるようです。
https://summerwind.jp/docs/rfc7540/#section6-8

The GOAWAY frame (type=0x7) is used to initiate shutdown of a connection or to signal serious error conditions.
GOAWAY allows an endpoint to gracefully stop accepting new streams while still finishing processing of previously established streams.

request.png

実際に確認すると、GOAWAYフレームは送信されておらず、HTTP/2の仕組みを利用せずTCPコネクションを終了させているようです。
https://github.com/grpc/grpc-go/blob/master/server.go#L578

func (s *Server) Serve(lis net.Listener) error {
  ...
    defer func() {
        s.mu.Lock()
        if s.lis != nil && s.lis[ls] {
            ls.Close()
            delete(s.lis, ls)
        }
        s.mu.Unlock()
    }()
  ...
}

このことから、rangeループでUnary RPCを複数回呼び出すよりも、Client Streaming RPCを利用した方が、効率が良さそうです。前者はTCPコネクションを確立、ストリームの作成、ストリームの終了、TCPコネクションの終了を繰り返すのに対して、後者は一度で済むからです。

所感

grpc-goの内部実装を調べることで、goroutineの扱い方、channelの使い方、HTTP2の仕組み、TCPの仕組みなどまとめて学ぶことができたのでよかったです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

10分でAndroidスマホにGo開発環境をたてる

Termuxのインストール

TermuxはAndroid用のターミナルエミュレータです。
Playストアにて「termux」を検索し、Termuxをインストールします。

Termuxを起動し以下のコマンドを実行します。

$ termux-setup-storage
$ apt update
$ apt upgrade

Go言語とgitのインストール

Termuxから、Goとgitをインストール。

$ apt install -y golang git

動作確認

Termuxにhello-worldアプリをgitクローンし、実行します。

$ git clone https://github.com/quzquz/go-http-hello-world.git
$ cd go-http-hello-world
$ go run main.go

少し待つと「Listening on port 8080」と表示されます。
Androidの適当なwebブラウザを起動しurlに以下を入力します。
http://localhost:8080

ブラウザに「Hello world!」と表示されます。

停止するには、Termux上でctrl+dを入力します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goを触ってみよう【aws-sdk-go】

まえがき

 もう結構使っている人も多いと思われるGo言語ですが、最近になってようやっと触りだしたこのごろです。きっかけはaws-sdkPythonboto3ライブラリを使ってインスタンス情報を抜き出そうとした時にdict型(辞書型)に翻弄された結果Goに逃げただけです。

やりたいこと

 上述の通り、Goでaws-sdkを利用して、インスタンス情報からInstance-typevCPUMemoryを抜き出してファイルに出力する!です。
背景としてはプロビジョニングツールとしてTerraformを利用しているのですが、DBのCloudwatch metricsをいちいち手入力するのはつらみなので、テンプレートで出せるようにしたかったためです。


突然のGopherくん!!!
Gopher
©Renée French
ref.) The Go Gopher

Goのインストール

 いわずもがな、まずは環境を用意しなければならないです。
個人的にハマったポイントをあらかじめ挙げておきます。

  • $GOPATHの設定
  • dep or Modules

インストール

まずは公式サイトよろしく、インストールしましょう。

Windowsの場合はインストーラーをダウンロードしてポチポチ
https://golang.org/dl/

MacやLinuxその他の場合はコマンドでもOK
Install.sh
tar -C /usr/local -xzf go$VERSION.$OS-$ARCH.tar.gz
#Macの場合はbrewでもOK
brew install go


Goのインストール完了後はgo get golang.org/dl/go$VERSIONで別バージョンをインストールすることも可能です。

$GOPATHの設定

結論からいくとどこでもいいらしい…が、おかげで嵌まってしまった。
参考:GOPATH は適当に決めて問題ない

今のところ個人の範囲でしか使っていないのであまり使わないような気がしていますが、結局 goenv にお任せすることにしました。
* Macの場合brew install goenv でもインストールできます。

Goインストール
$ goenv install --list
Available versions:
  1.2.2
  1.3.0
=======多いので割愛=======
  1.11.3
  1.11.4
  1.12beta1
 #1.12はまだbetaになってました
$ goenv install 1.11.4
Downloading go1.11.4.darwin-amd64.tar.gz...
-> https://dl.google.com/go/go1.11.4.darwin-amd64.tar.gz
Installing Go Darwin 64bit 1.11.4...
Installed Go Darwin 64bit 1.11.4 to /Users/Tetsu/.goenv/versions/1.11.4
確認
$ goenv global 1.11.4
$ go version
go version go1.11.4 darwin/amd64

Release

とりあえずHello World!

なにはともあれ、Hello World!をまずはやってみましょう。

以下のようなファイルを作って、

hello.go
package main
import "fmt"

func main(){
    fmt.Printf("Hello World!\n")
}

実行

go run
$ go run hello.go
Hello World!

buildしてバイナリにしちゃう。

go build
$ go build hello.go
$ ls -l hello
-rwxr-xr-x  1 User  staff  2020040  5 25 16:09 hello
$ ./hello
Hello World!

install してモジュール化もできます。

go install
$ ls -l ~/go/src/test
-rw-r--r--  1 User  staff  76  5 25 16:09 hello.go
$ go install test
$ ls -l ~/go/bin/test
-rwxr-xr-x  1 User  staff  2020040  5 25 16:18 /Users/Tetsu/go/bin/test
$  ~/go/bin/test
Hello World! 

depとModules

正直に言うとよくわかっていません。というよりこの後書くスクリプトが動けば良かったので、まだまだ必要な場面に出くわしていないだけです。。
depに関してはインストールすらまだな状態ですし、Modulesv1.12から本格参戦っぽいので…今からやるのであればModulesの方が良いと思います。
そのうちこのあたりについては、また記事書こうと思います。。

Goでaws-sdkを使ってみる

 まずはライブラリが必要なのでaws-sdk-goを入手しましょう。
aws-sdk-go-v2もありますがまだpreviewなのでやめておきました。
では公式ドキュメントに従ってインストールしてみます。

go get
$ go get -u github.com/aws/aws-sdk-go
go: finding github.com/aws/aws-sdk-go v1.16.3
go: finding github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
go: downloading github.com/aws/aws-sdk-go v1.16.3

実はこのときにModulesを利用してaws-sdk-goのインストールを実施してます。
参考:go1.11のmodulesの使い方について

go mod
$ mkdir tmp
$ cd tmp
tmp $
tmp $ go mod init github.com/own/test
go: creating new go.mod: module github.com/own/test
tmp $ ls -l
-rw-r--r--  1 User  staff  28  5 18 13:53 go.mod
tmp $ cat go.mod
module github.com/own/test

その後作成したスクリプトを実行すると…

go run rds_instance_parser.go
tmp $ go run $FILE_PATH/rds_instance_parser.go -args value.tf
go: downloading github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af
tmp $ ls -l
total 24
-rwxr-xr-x  1 User  staff  3826  5 18 13:58 value.tf
-rw-r--r--  1 User  staff    83  5 18 13:54 go.mod
-rw-r--r--  1 User  staff   408  5 18 13:54 go.sum

スクリプトの成果物とgo.sumファイルが出来上がっていました。
中身はこんな感じ。

go.sum
tmp $ cat go.sum
github.com/aws/aws-sdk-go v1.16.3 h1:esEQzoR8SVXtwg42nRoR/YLftI4ktsZg6Qwr7jnDXy8=
github.com/aws/aws-sdk-go v1.16.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=

どうやら上記スクリプト実行での依存関係にあるモジュールのバージョンをチェックした結果がgo.sumファイルに書き込まれているっぽい。
go.modにも以下が追記がされていたので、 こちらに必要なモジュールを追記しておけば依存解決もできるのかな?

go.mod
$ cat go.mod
module github.com/you/hello
# これが追記されていた
require github.com/aws/aws-sdk-go v1.16.3 // indirect

おまけ

ちなみに作ったスクリプトはこんな感じです。
rds_instance_parser.go
package main

import (
    "errors"
    "fmt"
    "io/ioutil"
    "os"
    "strconv"
    "strings"

    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/awserr"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/pricing"
)

func getProduct(nextToken string) (interface{}, interface{}, interface{}, *string, bool, error) {
    svc := pricing.New(session.New(&aws.Config{Region: aws.String("us-east-1")}))
    input := &pricing.GetProductsInput{
        Filters: []*pricing.Filter{
            {
                Field: aws.String("location"),
                Type:  aws.String("TERM_MATCH"),
                Value: aws.String("US East (N. Virginia)"),
            },
        },
        ServiceCode:   aws.String("AmazonRDS"),
        FormatVersion: aws.String("aws_v1"),
        MaxResults:    aws.Int64(1),
        NextToken:     aws.String(nextToken),
    }
    result, err := svc.GetProducts(input)
    if err != nil {
        if aerr, ok := err.(awserr.Error); ok {
            switch aerr.Code() {
            case pricing.ErrCodeInternalErrorException:
                fmt.Println(pricing.ErrCodeInternalErrorException, aerr.Error())
            case pricing.ErrCodeInvalidParameterException:
                fmt.Println(pricing.ErrCodeInvalidParameterException, aerr.Error())
            case pricing.ErrCodeNotFoundException:
                fmt.Println(pricing.ErrCodeNotFoundException, aerr.Error())
            case pricing.ErrCodeInvalidNextTokenException:
                fmt.Println(pricing.ErrCodeInvalidNextTokenException, aerr.Error())
            case pricing.ErrCodeExpiredNextTokenException:
                fmt.Println(pricing.ErrCodeExpiredNextTokenException, aerr.Error())
            default:
                fmt.Println(aerr.Error())
            }
        } else {
            // Print the error, cast err to awserr.Error to get the Code and
            // Message from an error.
            fmt.Println(err.Error())
        }
    }
    switch result.NextToken {
    case nil:
        return nil, nil, nil, nil, false, nil
    default:
        instanceType := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["instanceType"]
        if instanceType == nil {
            nToken := result.NextToken
            return nil, nil, nil, nToken, true, errors.New("InstanceType is nil")
        }
        vcpu := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["vcpu"]
        memory := result.PriceList[0]["product"].(map[string]interface{})["attributes"].(map[string]interface{})["memory"]
        nToken := result.NextToken
        return instanceType, vcpu, memory, nToken, true, nil
    }
}

func main() {
    type dbInstanceType struct {
        Name   interface{}
        Vcpu   interface{}
        Memory interface{}
    }
    type dbInstanceTypeList []dbInstanceType
    var dbInstanceTypes dbInstanceTypeList

    var nextToken = ""
    for i := 0; ; {
        i++
        instanceType, vcpu, memory, nToken, next, err := getProduct(nextToken)
        if err != nil {
            nextToken = *nToken
            continue
        }
        if next != true {
            break
        }
        dbInstanceTypeInfo := dbInstanceType{
            Name:   instanceType,
            Vcpu:   vcpu,
            Memory: memory,
        }
        dbInstanceTypes = append(dbInstanceTypes, dbInstanceTypeInfo)
        nextToken = *nToken
    }
    results := make([]dbInstanceType, 0, len(dbInstanceTypes))
    encountered := map[interface{}]bool{}
    for i := 0; i < len(dbInstanceTypes); i++ {
        if !encountered[dbInstanceTypes[i]] {
            encountered[dbInstanceTypes[i]] = true
            results = append(results, dbInstanceTypes[i])
        }
    }

    maxLength := 0
    for i := 0; i < len(results); i++ {
        if len(results[i].Name.(string)) > maxLength {
            maxLength = len(results[i].Name.(string))
        }
    }

    instanceSpecs := ""
    for i := 0; i < len(results); i++ {
        NAME := results[i].Name.(string)
        MEMORY := results[i].Memory.(string)
        // In the API, db.r5.4xlarge has a memory of 192 GiB, but correctly it is 128 GiB
        if NAME == "db.r5.4xlarge" {
            MEMORY = "128 GiB"
        }
        MEMORY = strings.Replace(MEMORY, " GiB", "", 1)
        s, _ := strconv.ParseFloat(MEMORY, 64)
        s = s * (1024 * 1024 * 1024)
        MEMORY = strconv.FormatFloat(s, 'f', 0, 64)
        if len(NAME) < maxLength {
            NAME = NAME + strings.Repeat(" ", maxLength-len(NAME))
        }
        VCPU := results[i].Vcpu.(string)
        SPACE := strings.Repeat(" ", 4-len(VCPU))
        instanceSpec := "    " + NAME + " = {cpu_cores = " + VCPU + "," + SPACE + "memory = " + MEMORY + "}\n"
        instanceSpecs = instanceSpecs + instanceSpec
    }

    content := []byte(
        "locals {\n" +
            "  instance_types = {\n" +
            instanceSpecs +
            "  }\n" +
            "}\n",
    )
    ioutil.WriteFile(os.Args[len(os.Args)-1], content, os.ModePerm)
}

一部出力結果抜粋

value.tf
 instance_types = {
     db.m4.4xlarge   = {cpu_cores = 16,  memory = 68719476736}
     db.r4.4xlarge   = {cpu_cores = 16,  memory = 130996502528}
     db.r3.xlarge    = {cpu_cores = 4,   memory = 32749125632}
 }

中身の解説は別の記事で書きます。
簡単に説明だけしておくとPricingのGetProductsというライブラリを利用して、バージニア北部からRDSインスタンスのスペックを抜き出して整形しているだけです。(バージニア北部から抜き出しているのは、対応しているインスタンスタイプが多いからです。)
冒頭Goに逃げたと書きましたが実はPythonでもできています。
…が、ごちゃごちゃしているので比較記事でもそのうち書きます。

おしまい

標準ライブラリでできることがかなり多いと思いました。(今回はほぼ使ってないけど。)
バックエンド系とかこのへんで実装してみたいなぁ…そのうち書きます。

宿題

次回予告的なのです。

  • depModules
  • Modulesについて(go.modとgo.sum)
  • PythonとGoの比較
  • Goのバックエンドへの実装
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goの静的解析ツールまとめ

まとめようと思ったきっかけ

Goを使いこなせる組織作りの取り組み / DeNA.go #1
https://speakerdeck.com/daisuzu/dena-dot-go-number-1?slide=10

こちらのスライドを見て、Goを始めたばかりの私はgofmtしか知らなかったので、この機会に調べまとめてみようと思いました。

今回まとめたツール一覧

  • gofmt
  • go fmt
  • goimports
  • go vet
  • golint

gofmt

  • 書いたコードを整形してくれる
引数 説明
-l フォーマットする必要のあるファイルを表示する
-w 修正内容を標準出力に書き出すのではなく、直接ファイルに書き込む
-d ファイルに書き込まずに、フォーマット前後のdiffを表示
-e すべての文法エラーを標準出力に書き出す。この引数を使わない場合は10行のエラーまでしか表示されない

ドキュメント

https://golang.org/cmd/gofmt/

go fmt

Fmt runs the command 'gofmt -l -w' on the packages named by the import paths. It prints the names of the files that are modified.

使い方

main.go
package main

import "fmt"

func main() {
fmt.Println("gofmt")
}
$ go fmt main.go
main.go

修正内容が標準出力に書き出されず、それを行ったファイル名が表示され、

$ cat main.go
package main

import "fmt"

func main() {
        fmt.Println("gofmt")
}

直接修正内容が反映されていることがわかります

goimports

  • importに関して、足りないimportを追加したり、使われていないimportを削除してくれる

ドキュメント

https://godoc.org/golang.org/x/tools/cmd/goimports

インストール

$ go get golang.org/x/tools/cmd/goimports

使い方

main.go
package main

import (
        "fmt"
        "os"
)

func main() {
        fmt.Println("goimports")
}

goimportsを実行(-wを付けないと標準出力に書き出し)

$ goimports main.go
package main

import (
        "fmt"
)

func main() {
        fmt.Println("goimports")
}

-wを付けると、importが整理された状態にファイルが変更されます

$ goimports -w main.go

go vet

  • goの標準パッケージに含まれる静的解析ツール
  • コンパイラによって検出されないエラーを見つけることができる
  • ただし、本当に問題あるかは保証できない

ドキュメント

https://golang.org/cmd/vet/

使い方

  • 単一のgoファイルに対して
$ go tool vet xxx.go
main.go
package main

import "fmt"

func main() {
        a := 0
        if a != 1 || a != 2 {
                a++
        }

        fmt.Printf("a = %s\n", a)
}
$ go tool vet main.go
main.go:7: suspect or: a != 1 || a != 2
main.go:11: Printf format %s has arg a of wrong type int
  • ディレクトリ内のすべてのgoファイルに対して
$ go tool vet [ディレクトリ]

golint

  • コードのコーディングスタイルの問題を検出する
  • エラーというより、「Suggestion(提案)」である

ドキュメント

https://github.com/golang/lint

インストール

$ go get golang.org/x/lint/golint

使い方

main.go
package main

import "fmt"

func Foo(bar int) int {
        if bar > 0 {
                return 1
        } else {
                return 0
        }
}

func main() {
        fmt.Println("golint")
        fmt.Println(Foo(1))
}
$ golint main.go
main.go:5:1: exported function Foo should have comment or be unexported
main.go:8:9: if block ends with a return statement, so drop this else and outdent its block

以下のように修正すると、

main.go
package main

import "fmt"

func Foo(bar int) int {
        if bar > 0 {
                return 1
        }

        return 0
}
func main() {
        fmt.Println("golint")
        fmt.Println(Foo(1))
}

二つ目の提案が消えました。

$ golint main.go
main.go:5:1: exported function Foo should have comment or be unexported

参考

http://blog.ralch.com/tutorial/golang-tools-inspection/

https://codeengineered.com/blog/2014/golint/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今日は暑かった

確かに動いた

  • @c-yan からもらったコメに従いサンプルコードを書換えたらエラーが表示さなくなった
  • 変更前
package main
import (
    "log"
    "net/http"
)
func main() {
    /* GETメソッドでURLにアクセス */
    res, err := http.Get("https://www.google.com/")
    if err != nil {
        log.Fatal(err)
    }
}

- 変更後

package main
import (
    "log"
    "net/http"
)
func main() {
    /* GETメソッドでURLにアクセス */
    _, err := http.Get("https://www.google.co.jp")
    if err != nil {
        log.Fatal(err)
    }
}

今日のメモ?コメント?ボヤキ?一言?

  • 勉強とは関係ない事は今後最後に書く
  • タイトルは「今日の一言」でいいかな?
    • 金曜日。出張で珍しく帰ってきたのが遅くなってしまい1日飛ばしてしまった、残念
    • 今日は今日とてこの炎天下のなか外出だったのでとても疲れた
    • 技術の話とは関連性は無いが、他の方時間の使い方や時間管理を参考にしたい
    • なんてタグで検索すればいいだろう。そのまま時間管理かな?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

golang × AWSIoT × Raspberrypi やってみた

前の記事ではRaspberrypiとAWSIoTを通信させる時、サンプルのPythonプログラムで動かしました。
その際、他にも通信をする方法あるのかなと探してみたところ、Goでも出来そうだったので試してみることにしました。
記事自体は環境構築含めたメモ程度に。

参考ページ

以下のページを参考にしています。

golang+MQTTでAWS IoTにPubslish

Raspberry PiにGo言語をインストールする

AWS IoT × Raspberrypi でやったことメモ

Raspberrypi側でGo開発環境をセットアップ

Goをインストール。(任意のバージョンで)

$ wget https://storage.googleapis.com/golang/go1.9.linux-armv6l.tar.gz
$ sudo tar -C /usr/local -xzf go1.9.linux-armv6l.tar.gz

インストール確認とバージョン確認を行う。

$ ls -l /usr/local/go
$ cat /usr/local/go/VERSION

パスの追加。
~/.bashrcファイル末尾に以下を追記。

export PATH=$PATH:/usr/local/go/bin

設定したパスの確認

$ source .bashrc
$ echo $PATH
$ which go

必要ライブラリのインストール

$ go get github.com/eclipse/paho.mqtt.golang
$ go get golang.org/x/net/websocket
$ go get golang.org/x/net/proxy

ソースコード

今回はお試しなので、参照サイトのものをそのまま使用。
GoPath等の設定は任意で設定してください。
(今回は分かりやすいように、home直下に全ファイル置いた書き方をしています)

main.go

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "io/ioutil"

    MQTT "github.com/eclipse/paho.mqtt.golang"
)

func NewTLSConfig() *tls.Config {
    // CA証明書を設定
    certpool := x509.NewCertPool()
    pemCerts, err := ioutil.ReadFile("xxxxxxxxx.pem")
    if err == nil {
        certpool.AppendCertsFromPEM(pemCerts)
    }

    // クライアント証明書とキーペアを設定
    cert, err := tls.LoadX509KeyPair("xxxxxxxxx-certificate.pem.crt", "xxxxxxxxx-private.pem.key")
    if err != nil {
        panic(err)
    }

    cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
    if err != nil {
        panic(err)
    }

    // config設定
    return &tls.Config{

        RootCAs: certpool,
        ClientAuth: tls.NoClientCert,
        ClientCAs: nil,
        InsecureSkipVerify: true,
        Certificates: []tls.Certificate{cert},
    }
}

var f MQTT.MessageHandler = func(client MQTT.Client, msg MQTT.Message) {
    fmt.Printf("TOPIC: %s\n", msg.Topic())
    fmt.Printf("MSG: %s\n", msg.Payload())
}

func main() {
    tlsconfig := NewTLSConfig()

    opts := MQTT.NewClientOptions()
    opts.AddBroker("ssl://xxxxxxxxxxxxxxxxxxx.amazonaws.com:8883")
    opts.SetClientID("ssl-sample").SetTLSConfig(tlsconfig)
    opts.SetDefaultPublishHandler(f)

    // 接続をする
    c := MQTT.NewClient(opts)
    if token := c.Connect(); token.Wait() && token.Error() != nil {
        panic(token.Error())
    }
    fmt.Println("AWS IoT Connect Success")

    if token := c.Publish("$aws/things/ESP32/shadow/update", 0, false,
        `{"state":{"reported":{"welcome":"I am gopher!!!"}}}`); token.Wait() && token.Error() != nil {
                panic(token.Error())
    }
    fmt.Println("Message Publish Success")

    // 切断
    c.Disconnect(250)
}

実行結果

result.png

result2.png

こんな感じで通信出来たみたいです。
Goは環境構築楽なので助かります。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む