20210125のGoに関する記事は3件です。

Go: MQTT の pub/sub

次のページを参考にしました。
GoでMQTTのpub/subを試す
ブローカーは broker.emqx.io です。

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

go get github.com/eclipse/paho.mqtt.golang
subscribe.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.sh
go run subscribe.go
publish.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.sh
go run publish.go
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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: gzip

Basic 認証

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 & パスワードを入れるとなると、確かに安全ではないですね。。

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

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 files

serger.goというgoファイルが生成されるので、早速起動させてみましょう。

$ go run server.go

下記アドレスにブラウザからアクセスすると、GraphQLのクエリを叩ける画面になります。
http://localhost:8080/

GraphQLのサーバーが、いとも簡単に立ってくれました。ここまでgolangは全く書いていません。確かにこれは便利ですね。
image.png

クエリの実行

早速、クエリ文を入力してみましょう。

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!]!
}

クエリが定義されていることを確認したなら一安心。実際にクエリを実行してみましょう。
画面の真ん中の実行ボタンをクリックします。
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3439343736322f61396236396464372d303263362d636230332d326566302d3764396233343330653435362e706e67.png

すると、実行部に次のようなメッセージが。

{
  "errors": [
    {
      "message": "internal system error",
      "path": [
        "todos"
      ]
    }
  ],
  "data": null
}

なるほど。datanullinternal system errorとなってしまっているようです。ふと思い返してみると、todosというクエリは、スキーマ(schema.graphql)の中に書いてあれど、レスポンスでなにを返すかは記述した覚えがありません。

そこで、graph/schema.resolvers.goの中身を見てみましょう

graph/schema.resolvers.go
package 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.go
func (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.go
func (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.go
package 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をリロードし、同じようにクエリを実行してみましょう。

image.png

今度は、先ほど定義した通りに、レスポンスが返ってくるはずです。

{
  "data": {
    "todos": [
      {
        "id": "1",
        "text": "サンプルテキスト"
      }
    ]
  }
}

さて、リゾルバで定義したキーは、ID, Text以外に、Done, Userがありました。
GraphQLベースのAPIは、クエリ側でレスポンスをコントロールできます。
DoneUserについての情報も取り出してみましょう!

query{
  todos{
    id
    text
    done
    user{
      id
      name
    }
  }
}

これを実行すると、レスポンスとして、リゾルバで定義したものがそのまま得られるはずです。

{
  "data": {
    "todos": [
      {
        "id": "1",
        "text": "サンプルテキスト",
        "done": true,
        "user": {
          "id": "001",
          "name": "山田太郎"
        }
      }
    ]
  }
}

自分でクエリを定義してみる

最後に自分でクエリを定義してみようと思います。
はじめに述べた通り、gqlgenはスキーマ第一的なアプローチに基づいています。
ですので、クエリを定義したくなった時、最初にやることは、「スキーマの定義」です。

サーバーを停止したら、graph/schema.graphqltype Query から始まる部分を編集してみましょう。

graph/schema.graphql
type Query {
  todos: [Todo!]!
  testquery: String!
}

スキーマの修正をresolverに反映するために、以下を実行します。

$ go run github.com/99designs/gqlgen generate

再び、graph/schema.resolver.goを見てみましょう。
先ほどは存在しなかったクエリ、Testqueryが存在しています!

graph/schema.resolver.go
func (r *queryResolver) Testquery(ctx context.Context) (string, error) {
    panic(fmt.Errorf("not implemented"))
}

自分の好きな文字列を返すように修正してみましょう。

graph/schema.resolver.go
func (r *queryResolver) Testquery(ctx context.Context) (string, error) {
    return "TestString", nil
}

もう一度サーバーを起動して、期待するレスポンスが返ってくるか確認してみましょう。

$ go run server.go
query{
  testquery
}
{
  "data": {
    "testquery": "TestString"
  }
}

image.png

以上の流れをステップごとにまとめると、以下のようになりますでしょうか。

  1. プロジェクト生成
  2. スキーマの編集
  3. リゾルバーの編集
  4. サーバーの起動

まとめ

Goの知識が少なかったり、Goでコードを書くのが面倒な人でも、gqlgenを使えば簡単にGraphQLのサーバーを作ることがわかりました。日本語で参照できる記事が不足しているだけで、使い慣れたら半端なく便利なものかと思います。

※ 偉そうに記事を書いている私も初心者同然ですので、明らかにおかしな点やアップデートが必要な部分があればご指摘いただけますと幸いです。

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