- 投稿日:2021-01-25T15:47:48+09:00
Go: MQTT の pub/sub
次のページを参考にしました。
GoでMQTTのpub/subを試す
ブローカーは broker.emqx.io です。ライブラリーのインストール
go get github.com/eclipse/paho.mqtt.golangsubscribe.go// --------------------------------------------------------------- // // subscribe.go // // Jan/25/2021 // --------------------------------------------------------------- package main import ( "fmt" "log" "os" "os/signal" mqtt "github.com/eclipse/paho.mqtt.golang" ) // --------------------------------------------------------------- func main() { fmt.Fprintf (os.Stderr,"*** 開始 ***\n") msgCh := make(chan mqtt.Message) var f mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) { msgCh <- msg } opts := mqtt.NewClientOptions() opts.AddBroker("tcp://broker.emqx.io:1883") cc := mqtt.NewClient(opts) if token := cc.Connect(); token.Wait() && token.Error() != nil { log.Fatalf("Mqtt error: %s", token.Error()) } if subscribeToken := cc.Subscribe("go-mqtt/sample", 0, f); subscribeToken.Wait() && subscribeToken.Error() != nil { log.Fatal(subscribeToken.Error()) } signalCh := make(chan os.Signal, 1) signal.Notify(signalCh, os.Interrupt) for { select { case m := <-msgCh: fmt.Printf("topic: %v, payload: %v\n", m.Topic(), string(m.Payload())) case <-signalCh: fmt.Printf("Interrupt detected.\n") cc.Disconnect(1000) return } } } // ---------------------------------------------------------------実行コマンド
subscribe.shgo run subscribe.gopublish.sh// --------------------------------------------------------------- // // publish.go // // Jan/25/2021 // --------------------------------------------------------------- package main import ( "os" "fmt" "log" "time" mqtt "github.com/eclipse/paho.mqtt.golang" ) // --------------------------------------------------------------- func main() { fmt.Fprintf (os.Stderr,"*** 開始 ***\n") opts := mqtt.NewClientOptions() opts.AddBroker("tcp://broker.emqx.io:1883") cc := mqtt.NewClient(opts) if token := cc.Connect(); token.Wait() && token.Error() != nil { log.Fatalf("Mqtt error: %s", token.Error()) } for it := 0; it < 5; it++ { now := time.Now () text := fmt.Sprintf("こんにちは %d ", it) + fmt.Sprintf ("%s",now) token := cc.Publish("go-mqtt/sample", 0, false, text) token.Wait() } cc.Disconnect(250) fmt.Println("Complete publish") fmt.Fprintf (os.Stderr,"*** 終了 ***\n") } // ---------------------------------------------------------------実行コマンド
publish.shgo run publish.go
- 投稿日:2021-01-25T05:24:53+09:00
[Go] GET リクエスト & レスポンスの取り扱い方
サーバー側の実装例をよくみかけますがk、GO ではクライアント側の実装もできます。
基本的な GET リクエスト
まずは、基本から。
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { url := "http://localhost:8080" resp, err := http.Get(url) if err != nil { log.Fatalln("Failed to get response") } content, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalln("Failed to read content") } defer resp.Body.Close() fmt.Println(string(content)) }netcat を使ってリクエスト内容を確認 (=
nc -l 8080
)。GET / HTTP/1.1 Host: localhost:8080 User-Agent: Go-http-client/1.1 Accept-Encoding: gzip
標準では
User-Agent: Go-http-client/1.1
ですね。リクエストヘッダーにカスタムの情報を追加・編集
User-Agent
を別の情報で表示する場合は、ヘッダーの編集が必要package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { url := "http://localhost:8080" // Client 型を取得 client := http.DefaultClient // Request 型 (ポインタ) を取得 req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalln("Failed to create reqeust") } // 任意の Header を作る。ここでは、Agent 名を変更 req.Header.Add("User-Agent", "Custom Agent") // リクエストを送信、レスポンスを resp に入れる resp, err := client.Do(req) if err != nil { log.Fatalln("Failed to get response") } content, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalln("Failed to read content") } defer resp.Body.Close() fmt.Println(string(content)) }netcat を使ってリクエスト内容を確認 (=
nc -l 8080
)。GET / HTTP/1.1 Host: localhost:8080 User-Agent: Custom Agent # ちゃんと変わってる Accept-Encoding: gzipBasic 認証
Basic 認証の仕組みを理解するついでに調べました。
package main import ( "fmt" "io/ioutil" "log" "net/http" ) func main() { url := "http://localhost:8080" req, err := http.NewRequest("GET", url, nil) if err != nil { log.Fatalln("Failed to create request") } // Basic 認証用の ID & Password req.SetBasicAuth("user", "passw0rd") resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatalln("Failed to get response") } content, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalln("Failed to read content") } defer resp.Body.Close() fmt.Println(string(content)) fmt.Println(resp.StatusCode) }netcat を使ってリクエスト内容を確認 (=
nc -l 8080
)。GET / HTTP/1.1 Host: localhost:8080 User-Agent: Go-http-client/1.1 Authorization: Basic dXNlcjpwYXNzdzByZA== # base64 デコードすると user:passw0rd Accept-Encoding: gzipヘッダーに base64 エンコードされた ID & パスワードを入れるとなると、確かに安全ではないですね。。
- 投稿日:2021-01-25T01:24:35+09:00
Go初心者のgqlgen事始め
はじめに
起業している友人が猫の手も借りたいと言うことで、GraphQL関連のお仕事を手伝うことになりました。当時はgolangもGraphQLも全くの初心者で、猫の手より役立たずだったことを記憶しています。
私のスキルが足らなかったのももちろんですが、慣れているPythonやJSなどにくらべて、初心者に優しい記事が少なかったのも一因であるように感じました。
参照しやすい情報が少ないと言うのも、GoやGraphQLの参入障壁が高い理由のひとつかもしれませんね。ということで、私のような初心者でもすぐにGo + GraphQLに取り組めるように、
備忘録程度ですが、gqlgenについて書き記しておこうと思います。※ Goの基本的な文法や、GraphQLの概念については、ある程度わかっている前提で話が進みます。もし、これらのことに自信がなかったら、以下の記事を参考にしながら読むと幸せになれるかもしれません。
環境
OS
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H114
Go
$ go version go version go1.15.5 darwin/amd64
gqlgenとは
公式テキストを引用するに限ります。
gqlgen is a Go library for building GraphQL servers without any fuss.
gqlgen is based on a Schema first approach — You get to Define your API using the GraphQL Schema Definition Language.
gqlgen prioritizes Type safety — You should never see map[string]interface{} here.
gqlgen enables Codegen — We generate the boring bits, so you can focus on building your app quickly.英語アレルギーの人用に、意訳を交えて日本語に直してみました。
gqlgenとはGraphQLサーバーを簡単に構築するためのgolangのライブラリです。
gqlgenは「スキーマ第一」的なアプローチに基づいています。--- APIをGraphQLのスキーマ定義言語に基づいて定義します。
gqlgenは型の安全性を優先します。 ---map[string]interface{}
文を見なくて済むようになります。
gqlgenはコードの生成を可能にします。 --- 退屈なコード断片を書かなくて済むので、アプリケーションを速やかに構築することに集中できます。要は、GraphQLサーバーが簡単に建てられるようになるGoのライブラリってことです。
どれだけ簡単か、実際にgqlgenを動かしながら流れを見てみましょう。GraphQLサーバーを建ててみよう
プロジェクト生成~起動まで
とりあえず、GraphQLが動かせるところまで行ってみましょう。
今回は、gqlgen-testというプロジェクトを作ってみます。# ディレクトリ作成 $ mkdir gqlgen-test # Goプロジェクトとして初期化 $ go mod init gqlgen-test # プロジェクト内に移動 $ cd gqlgen-test # gqlgenをGOPATH/bin配下におく $ go get github.com/99designs/gqlgenこの状態ではまだ、ディレクトリ内にはファイルが存在しない状況です。
$ ls次に、gqlgenを使って、GraphQLサーバーの骨組みを自動生成してみましょう。
# gqlgen initにより、GraphQLサーバーの骨組みを生成 $ go run github.com/99designs/gqlgen initすると、以下のような構成になります。
(以下はtree
コマンドのアウトプットです。)├── gqlgen.yml ├── graph │ ├── generated │ │ └── generated.go │ ├── model │ │ └── models_gen.go │ ├── resolver.go │ ├── schema.graphqls │ └── schema.resolvers.go └── server.go 3 directories, 7 filesserger.goというgoファイルが生成されるので、早速起動させてみましょう。
$ go run server.go
下記アドレスにブラウザからアクセスすると、GraphQLのクエリを叩ける画面になります。
http://localhost:8080/GraphQLのサーバーが、いとも簡単に立ってくれました。ここまでgolangは全く書いていません。確かにこれは便利ですね。
クエリの実行
早速、クエリ文を入力してみましょう。
query{ todos{ id text } }これは、「
todos
と言う名前で定義されたクエリを実行し、idとtextを返す」ことを意味します。
「todos
と言う名前のクエリなんぞ、定義した覚えはないぞ」と言う人は、graph/schema.graphql
を覗いてみてください。graph/schema.graphql# GraphQL schema example # # https://gqlgen.com/getting-started/ type Todo { id: ID! text: String! done: Boolean! user: User! } type User { id: ID! name: String! } type Query { todos: [Todo!]! } input NewTodo { text: String! userId: String! } type Mutation { createTodo(input: NewTodo!): Todo! }たしかに
todos
を定義している部分があります。
go run github.com/99designs/gqlgen init
を行った時に、自動でここまで生成してくれていたのですね。type Query { todos: [Todo!]! }クエリが定義されていることを確認したなら一安心。実際にクエリを実行してみましょう。
画面の真ん中の実行ボタンをクリックします。
すると、実行部に次のようなメッセージが。
{ "errors": [ { "message": "internal system error", "path": [ "todos" ] } ], "data": null }なるほど。
data
がnull
でinternal system error
となってしまっているようです。ふと思い返してみると、todos
というクエリは、スキーマ(schema.graphql
)の中に書いてあれど、レスポンスでなにを返すかは記述した覚えがありません。そこで、
graph/schema.resolvers.go
の中身を見てみましょうgraph/schema.resolvers.gopackage graph // This file will be automatically regenerated based on the schema, any resolver implementations // will be copied through when generating and any unknown code will be moved to the end. import ( "context" "fmt" "gqlgen-test/gqlgen-test/graph/generated" "gqlgen-test/gqlgen-test/graph/model" ) func (r *mutationResolver) CreateTodo(ctx context.Context, input model.NewTodo) (*model.Todo, error) { panic(fmt.Errorf("not implemented")) } func (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { panic(fmt.Errorf("not implemented")) } // Mutation returns generated.MutationResolver implementation. func (r *Resolver) Mutation() generated.MutationResolver { return &mutationResolver{r} } // Query returns generated.QueryResolver implementation. func (r *Resolver) Query() generated.QueryResolver { return &queryResolver{r} } type mutationResolver struct{ *Resolver } type queryResolver struct{ *Resolver }Todosという関数は、エラー文を吐き出すのみで、レスポンスとして何かを返すような仕様にはなっていません。
graph/schema.resolvers.gofunc (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { panic(fmt.Errorf("not implemented")) }レスポンスを定義してみよう
schema.resolvers.go
のように、データを操作し、レスポンスとして返すものを決めている部分を、リゾルバと呼びます。
リゾルバを編集し、レスポンスを定義してみましょう。いったん
Ctrl + C
で先ほど立ち上げたサーバーを停止させます。
停止し終えたら、schema.resolver.go
を以下のように編集してみます。graph/schema.resolver.gofunc (r *queryResolver) Todos(ctx context.Context) ([]*model.Todo, error) { todo := []*model.Todo{ { ID: "1", Text: "サンプルテキスト", Done: true, User: &model.User{ ID: "001", Name: "山田太郎", }, }, } return todo, nil }※ 本来ならばここは DBの操作などを行う部分ですが、今回はGraphQLをとりあえず動かしてみることを主な目的としています。説明の簡略化のため固定レスポンスをベタ書きしてます。DBとの接続部分は、また別の記事で書けたら書きたいです。
同様に、
resolver.go
も以下のように編集しておきましょう。graph/resolver.gopackage graph import "gqlgen-test/gqlgen-test/graph/model" // This file will not be regenerated automatically. // // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { todos []*model.Todo }リゾルバを編集し終えたら、プロジェクトのルートに戻り、再びサーバーを起動します。
$ go run server.go
localhost:8080をリロードし、同じようにクエリを実行してみましょう。
今度は、先ほど定義した通りに、レスポンスが返ってくるはずです。
{ "data": { "todos": [ { "id": "1", "text": "サンプルテキスト" } ] } }さて、リゾルバで定義したキーは、
ID
,Text
以外に、Done
,User
がありました。
GraphQLベースのAPIは、クエリ側でレスポンスをコントロールできます。
Done
やUser
についての情報も取り出してみましょう!query{ todos{ id text done user{ id name } } }これを実行すると、レスポンスとして、リゾルバで定義したものがそのまま得られるはずです。
{ "data": { "todos": [ { "id": "1", "text": "サンプルテキスト", "done": true, "user": { "id": "001", "name": "山田太郎" } } ] } }自分でクエリを定義してみる
最後に自分でクエリを定義してみようと思います。
はじめに述べた通り、gqlgenはスキーマ第一的なアプローチに基づいています。
ですので、クエリを定義したくなった時、最初にやることは、「スキーマの定義」です。サーバーを停止したら、
graph/schema.graphql
のtype Query
から始まる部分を編集してみましょう。graph/schema.graphqltype Query { todos: [Todo!]! testquery: String! }スキーマの修正をresolverに反映するために、以下を実行します。
$ go run github.com/99designs/gqlgen generate
再び、
graph/schema.resolver.go
を見てみましょう。
先ほどは存在しなかったクエリ、Testquery
が存在しています!graph/schema.resolver.gofunc (r *queryResolver) Testquery(ctx context.Context) (string, error) { panic(fmt.Errorf("not implemented")) }自分の好きな文字列を返すように修正してみましょう。
graph/schema.resolver.gofunc (r *queryResolver) Testquery(ctx context.Context) (string, error) { return "TestString", nil }もう一度サーバーを起動して、期待するレスポンスが返ってくるか確認してみましょう。
$ go run server.go
query{ testquery }{ "data": { "testquery": "TestString" } }以上の流れをステップごとにまとめると、以下のようになりますでしょうか。
- プロジェクト生成
- スキーマの編集
- リゾルバーの編集
- サーバーの起動
まとめ
Goの知識が少なかったり、Goでコードを書くのが面倒な人でも、gqlgenを使えば簡単にGraphQLのサーバーを作ることがわかりました。日本語で参照できる記事が不足しているだけで、使い慣れたら半端なく便利なものかと思います。
※ 偉そうに記事を書いている私も初心者同然ですので、明らかにおかしな点やアップデートが必要な部分があればご指摘いただけますと幸いです。