- 投稿日:2019-05-26T23:05:25+09:00
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
(補間、内挿)機能はありません。つまり、文字列内で変数をパース(展開)する機能は標準では持っていません。コンパイル型の言語なので工夫が必要です。
python
3.6 より前のように、変数と文字列を連結演算子(+
)でつなげる。- 文字列フォーマット関数
Sprintf
を使う。- 大量に置き換える場合は、
text
やhtml
ライブラリのtemplate
関数を使う。- または、
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() }
- 上記の動作をオンラインで動作を見る @ paiza.IO
外部ライブラリを使った例
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.gopackage 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/template
やhtml/template
を使うべし、というのは理解できる。でも、やはり、Python3.6 でそうなったように、Golang も変数の文字列内展開を標準にして欲しいなぁ。参考文献
- 「Goで文字列中で変数展開する」 @ Qiita
- 「String interpolation in golang」@ Medium
- 投稿日:2019-05-26T21:58:34+09:00
内部実装から理解するgRPC
概要
目的
gRPCはDocumentにあるように以下の特徴があるかと思います。
- protocol buffer のようなインターフェース定義語 (IDL) から生成されたコードを利用してRPCができる
- HTTP/2で通信することができ、リクエストとレスポンスをそれぞれ分割できる
- 多言語に対応している
しかし、この記事ではこれらの機能の紹介ではなく、gRPCの仕組みを理解することを意図しています。
なぜそれを意図したかというと普段の開発でgRPCを利用しているものの、どのような仕組みでRPCが実現できているのかイメージが持てていなかったためです。そのために、grpc-goの内部実装(2019/5時点)を読み解きながら、実際の通信の中身を覗いてみました。そして結果的には以下の効用がありました。
- protoc-gen-goがprotocol-buffersから生成したコードがどのように利用されているか分かる
- HTTP/2により多重化されたリクエストをどう扱っているか分かる
- Webサーバーを実装をする時にどのような設計にすればいいのか参考になる
処理流れ
まずソースコードを読むとサーバー側は以下のようなフローで処理されていることが分かります。
goroutineが三重に実行されています。具体的には以下の順番で処理が進みます。
- TCP Connection の受付
- TLSハンドシェイクを並行処理
- フレームの読み取りを並行処理
- 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の実装においてもこれらがなされています。
まず、これらのプロセスを理解するためにHTTP/2に関する用語を確認します。
- ストリーム
- 確立した接続内の双方向の論理的なフレームの流れのまとまりを表現したもの
- メッセージ
- 論理的なリクエストまたはレスポンス、メッセージを表現するためにフレームを順番にまとめたもの
- フレーム
- HTTP/2における通信の最小単位で、それぞれのフレームはヘッダを持ち、ヘッダはそのフレームが所属するストリームを識別する
メッセージとフレームの関係は従来の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#L148func 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#L426func (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 ... }要するに以下のような構造体にそれぞれ作られることになります。
そのように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.gofunc 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#L133func (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を使って、この通信をみてみます。
複数のフレームが送受信されていることが確認できます。そして全てのフレームはStreamIDが1になっており、一つのストリームで通信されています。
gRPC Server 側の挙動を確認する
今度はメソッド呼び出しされたgRPCサーバー側の挙動を確認します。
概要で説明したように以下の流れでgRPC method の呼び出しが行われます。
- TCP Connection の確立
- TLSハンドシェイクを並行処理
- Streamの読み取りを並行処理
- 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#L545func (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#L639func (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#L713func (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のペアを持ちます。そして、そのフレームを受け取ると、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フレームが送信されます。
フレームタイプがDATA(http2.DataFrame)の場合は、http2Server.handleDataが実行されます。
ここでは、フレームからtransport.Stream構造体を取得して、そこに登録されたChannelに対して読み取ったフレームのデータを書き込んでいます。
https://github.com/grpc/grpc-go/blob/master/internal/transport/http2_server.go#L544func (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#L71func (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#L713func (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名がセットされています。それをstream.Method()で取り出しているので以下のような結果が返ってきます。
/helloworld.Greeter/SayHello次にそれを使って、gRPCサーバー起動前にgrpc.Serverのmフィールドに登録したgrpc.service構造体を取り出します。
これは最初に説明したように、Service名とgrpc.MethodDesc構造体を対応付けており、さらにmethod名から対応するgRPC methodを取得できます。そして、Server.processUnaryRPCメソッドを呼び出します。
https://github.com/grpc/grpc-go/blob/master/server.go#L1248func (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-8The 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.実際に確認すると、GOAWAYフレームは送信されておらず、HTTP/2の仕組みを利用せずTCPコネクションを終了させているようです。
https://github.com/grpc/grpc-go/blob/master/server.go#L578func (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の仕組みなどまとめて学ぶことができたのでよかったです。
- 投稿日:2019-05-26T20:17:26+09:00
10分でAndroidスマホにGo開発環境をたてる
Termuxのインストール
TermuxはAndroid用のターミナルエミュレータです。
Playストアにて「termux」を検索し、Termuxをインストールします。
Termuxを起動し以下のコマンドを実行します。
$ termux-setup-storage $ apt update $ apt upgradeGo言語と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を入力します。
- 投稿日:2019-05-26T17:56:28+09:00
Goを触ってみよう【aws-sdk-go】
まえがき
もう結構使っている人も多いと思われるGo言語ですが、最近になってようやっと触りだしたこのごろです。きっかけは
aws-sdk
でPython
のboto3ライブラリを使ってインスタンス情報を抜き出そうとした時にdict型(辞書型)に翻弄された結果Go
に逃げただけです。やりたいこと
上述の通り、Goでaws-sdkを利用して、インスタンス情報から
Instance-type
、vCPU
、Memory
を抜き出してファイルに出力する!です。
背景としてはプロビジョニングツールとしてTerraformを利用しているのですが、DBのCloudwatch metricsをいちいち手入力するのはつらみなので、テンプレートで出せるようにしたかったためです。
突然のGopherくん!!!
©Renée French
ref.) The Go GopherGoのインストール
いわずもがな、まずは環境を用意しなければならないです。
個人的にハマったポイントをあらかじめ挙げておきます。
$GOPATH
の設定dep
orModules
インストール
まずは公式サイトよろしく、インストールしましょう。
MacやLinuxその他の場合はコマンドでもOK
Install.shtar -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とりあえずHello World!
なにはともあれ、
Hello World!
をまずはやってみましょう。以下のようなファイルを作って、
hello.gopackage 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に関してはインストールすらまだな状態ですし、Modulesもv1.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.gotmp $ 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.sumtmp $ 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.gopackage 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.tfinstance_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でもできています。
…が、ごちゃごちゃしているので比較記事でもそのうち書きます。おしまい
標準ライブラリでできることがかなり多いと思いました。(今回はほぼ使ってないけど。)
バックエンド系とかこのへんで実装してみたいなぁ…そのうち書きます。宿題
次回予告的なのです。
- dep と Modules
- Modulesについて(go.modとgo.sum)
- PythonとGoの比較
- Goのバックエンドへの実装
- 投稿日:2019-05-26T12:37:31+09:00
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行のエラーまでしか表示されない ドキュメント
go fmt
gofmt -l -w
のこと- 修正内容を標準出力に書き出すのではなく、直接ファイルに書き込み、それを行ったファイル名を表示する
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.gopackage 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.gopackage 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.gogo vet
- goの標準パッケージに含まれる静的解析ツール
- コンパイラによって検出されないエラーを見つけることができる
- ただし、本当に問題あるかは保証できない
ドキュメント
使い方
- 単一のgoファイルに対して
$ go tool vet xxx.gomain.gopackage 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.gopackage 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.gopackage 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参考
- 投稿日:2019-05-26T01:41:08+09:00
今日は暑かった
確かに動いた
- @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) } }
- じゃあ
res
って何?- 検索したら要件を実現出来る方法に近い内容が出てきた。明日以降こちらを参考にまた進めよう
今日のメモ?コメント?ボヤキ?一言?
- 勉強とは関係ない事は今後最後に書く
- タイトルは「今日の一言」でいいかな?
- 金曜日。出張で珍しく帰ってきたのが遅くなってしまい1日飛ばしてしまった、残念
- 今日は今日とてこの炎天下のなか外出だったのでとても疲れた
- 技術の話とは関連性は無いが、他の方時間の使い方や時間管理を参考にしたい
- なんてタグで検索すればいいだろう。そのまま時間管理かな?
- 投稿日:2019-05-26T00:41:22+09:00
golang × AWSIoT × Raspberrypi やってみた
前の記事ではRaspberrypiとAWSIoTを通信させる時、サンプルのPythonプログラムで動かしました。
その際、他にも通信をする方法あるのかなと探してみたところ、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) }実行結果
こんな感じで通信出来たみたいです。
Goは環境構築楽なので助かります。