20210315のGoに関する記事は6件です。

【Golang】標準入力から1文字のキー入力を読み込む(enter いらずの On KeyPress/KeyDown っぽいやつ)

Go言語で標準入力のキー押下(KeyPress)を検知する

Golang で標準入力から1行ごとではなく、1文字だけ取得する方法を知りたい。

正確には TTY からの標準入力を 1 文字欲しいのです。つまり、ターミナルやコマンドラインなどでのユーザー入力(標準入力)で y -> enter でなく、y キーを押しただけで y を検知したいのです。

「golang 標準入力 1文字 読み込む」で Qiita 記事に絞ってググっても、競プロの課題にあるような「改行区切りで得られた文字列から、最初の文字を取得する」ような例やパイプ渡しの例ばかりだったので、自分のググラビリティとして。

TL; DR (今北産業)

  1. @mattn さんの素晴らしい github.com/mattn/go-tty パッケージの tty.ReadRune() を使う。
  2. こういうこと ? をしたい人向け

    $ go run . <enter>
    Key press => a
    Key press => b
    Key press => c
    Key press => d
    ...(ctrl+c)
    signal: interrupt
    
  3. 動作確認済み環境

    • Intel Core i5(2.7 GHz デュアルコア)
      • macOS Catalina 10.15.7, Debian GNU/Linux 10 (buster), Alpine Linux v3.13
    • ARMv7 Processor rev 4 (v7l)
      • Raspbian GNU/Linux 9 (stretch),
    • Go version 1.16

TS; DR (マスター、動くものをくれ)

main.go
package main

import (
    "fmt"
    "log"

    "github.com/mattn/go-tty"
)

func main() {
    tty, err := tty.Open()
    if err != nil {
        log.Fatal(err)
    }

    defer tty.Close()

    fmt.Println("Ready. Press any key ...")

    for {
        r, err := tty.ReadRune()
        if err != nil {
            log.Fatal(err)
        }
        fmt.Println("Key press => " + string(r))
    }
}
go.mod
module sample/tty

go 1.16

require github.com/mattn/go-tty v0.0.3

Go Playground では tty からの標準入力のテストができないため、再現性のために Docker も置いておきます。

macOS とラズパイ3+B(Raspbian GNU/Linux 9, stretch)の Docker で動きました。あと、Docker 内で darwinarm 向けにビルドすれば Golang 入ってなくてもローカルでバイナリ動きました。

Dockerfile
FROM golang:1.16-alpine

COPY . /app
WORKDIR /app
RUN go mod tidy
macOS,Linuxのターミナル(要Docker)
$ # ファイルの確認
$ tree .
.
├── Dockerfile
├── go.mod
└── main.go

0 directories, 2 files

$ # 実行環境(コンテナ・イメージ)のビルド
$ docker build --tag sample/tty .
...

$ # `go run .` と同じことをコンテナ内で実行
$ docker run --rm --interactive --tty sample/tty go run .
Ready. Press any key ...
Key press => a
Key press => b
Key press => c
Key press => d
Key press => e
(ctrl+c)
signal: interrupt
$
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

<Golang+Nginx+MySQL>Macローカル上にDocker Composeでアプリを立ち上げてみた

Dockerアプリケーションのアーキテクチャー

スクリーンショット 2021-03-15 10.32.57.png

ざっくりとやること・できることの説明

  • docker-composeでNginxとGo言語のAPI, DBとしてMySQLのコンテナを3つ立ち上げる
  • Nginxのindex.htmlからGo言語で書いたREST API(go-ginのフレームワーク使用)を経由して、DBに問い合わせ
  • DBにCRUD(gormのフレームワーク使用)ができることを確認
  • ※DockerとDocker Composeはインストールされているものとする。

バージョン確認

確認してみたら、以下のバージョンでした。

Docker version 20.10.2
docker-compose version 1.27.4
golang:1.14.7-alpine3.12
mysql:5.7
nginx:1.17.3-alpine

※厳密にはバージョンの依存関係とかも考えないといけないが、一旦ローカルで動かすので、スルーしますm(__)m

フォルダ構成

docker-local
├── .env
├── api
│   └── src
│       ├── main.go
│       ├── model  ── model.go
│       └── config ── config.go
├── db
│   ├── conf ── my.cnf
│   └── db-data
├── docker
│   └── nginx
│       ├── Dockerfile
│       └── nginx.conf
├── docker-compose.yml
└── web
    ├── env
    │   └── nginx ── site.conf
    └ src ── index.html

ディレクトリとファイルの用途解説

名前 種類 説明
docker-local フォルダ dockerローカルアプリケーションの親フォルダ
.env ファイル docker-composeで使う環境変数のファイル
api フォルダ APIのフォルダ
src フォルダ ソースフォルダ
main.go ファイル goを起動するときのファイル
model/model.go ファイル DBとやりとりする時のファイル
struct, dao系のメソッドを記載
config/config.go ファイル DBの接続情報等を記載
db フォルダ DBに関するフォルダ
conf/my.cnf ファイル 任意のDB設定を記載。マウント用
db-data フォルダ DBのデータをsyncするマウント用
docker フォルダ Dockerfileたちを格納するフォルダ
nginx フォルダ NginxのimageをビルドするDockerfileを格納
Dockerfile ファイル nginxのimage
nginx.conf ファイル nginxの設定ファイル
docker-compose.yml ファイル docker containerを一括でコントロールするyamlファイル
web フォルダ フロントのファイルを格納
env フォルダ 環境設定ファイルを格納
nginx/site.conf ファイル nginxのserver設定ファイル
IPアドレス、ポート、ルーティングなど設定
src/index.html ファイル 画面表示用ファイル

Docker Composeの中身

version: "3.8"

services:
  nginx:
    image: infra-challenge:nginx-20210312
    # command: 
    build: 
      context: ./docker/nginx
      dockerfile: Dockerfile
    container_name: nginx
    ports:
      - 80:80
    volumes:
      - ./web/env/nginx:/etc/nginx/conf.d
      - ./web/src:/var/www/html
    depends_on:
      - api
    logging:
      driver: "none"
    restart: always
    networks:
      - services

  api:
    image: golang:1.14.7-alpine3.12
    command: go run main.go --host 0.0.0.0 --port 8080
    container_name: api
    ports:
      - 8080:8080
    volumes:
      - ./api:/go/src/github.com/infra-challenge/api
      - /Users/username/go:/go
    working_dir: /go/src/github.com/infra-challenge/api/src
    environment:
      - APP_STAGE=local
      - LOG_LEVEL=debug
      - MYSQL_CONNECTION
    depends_on:
      - db
    restart: always
    networks:
      - services

  db:
    image: mysql:5.7
    # build: ./docker/mysql
    container_name: db
    ports:
      - 3306:3306
    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/db-data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD
      - MYSQL_DATABASE
      - MYSQL_USER
      - MYSQL_PASSWORD
    healthcheck:
      test: ["CMD-SHELL", "mysqlshow -u ${MYSQL_USER} -p${MYSQL_PASSWORD}"]
      interval: "5s"
      retries: 10
    logging:
      driver: "none"
    restart: always
    networks:
      - services

networks:
  services:
    external:
      name: local_infra_challenge_networks

主な属性の解説

  • docker composeの文法バージョンは3.8だよ
  • services配下に、コンテナの定義を書いていく。今回はnginx, api, dbの3つ
  • nginxDockerfileを使うから、image名は任意で、buildの属性を指定。
    • contextはDockerfileのある相対パス、dockerfileはDockerfile名
    • ※ファイル名がDockerfileとなっていたら、build: ./docker/nginxとしてもOK
  • container_nameを指定すると、docker stopとかするときに、idじゃなくて名前を指定すれば制御できるので便利
  • ports<ホスト(Macローカル)>:<ゲスト(Dockerコンテナ)>でポートを接続
  • volumes<ホスト(Macローカル)>:<ゲスト(Dockerコンテナ)>でマウントフォルダを指定
    • マウントとは、フォルダ同士を共有してsync(同期)すること。
  • depends_onは他のコンテナが起動成功でき次第〜みたいなやつ
  • apiとdbは、自分でDockerfileは用意せずに、Docker-Hub公式のimageを使っている
  • environmentに環境変数を指定する。
    • docker-compose.ymlと同階層に.envファイルを置き指定
  • DBではhealthcheckを行い、起動しているか随時確認
  • ネットワークを組むことで、コンテナ間通信が可能となり、APIからDBに問い合わせができるようになった

ネットワークについて

作ったネットワークの情報を確認してみると、下記のようなObjectが返ってきますね〜
"Subnet": "172.19.0.0/16",の中で、Containers(nginx, api, db)がIPアドレスを振られて存在しているのがわかります。
ちなみに、networksだけでなくlinksという属性もあり、linksを使うと、一方的にコンテナ間通信ができるようになる設定もあるので、TPOによって使い分ける◎
公式docker:links

$ docker network inspect local_infra_challenge_networks
[
    {
        "Name": "local_infra_challenge_networks",
        "Id": "f6a978e6f3aa803be061418c1c7081d132c9538c207ea08b1d732fc55ba2b917",
        "Created": "2021-03-12T03:57:20.0697997Z",
        "Scope": "local",
        "Driver": "bridge",
        "EnableIPv6": false,
        "IPAM": {
            "Driver": "default",
            "Options": {},
            "Config": [
                {
                    "Subnet": "172.19.0.0/16",
                    "Gateway": "172.19.0.1"
                }
            ]
        },
        "Internal": false,
        "Attachable": false,
        "Ingress": false,
        "ConfigFrom": {
            "Network": ""
        },
        "ConfigOnly": false,
        "Containers": {
            "7a81d8c6f965a07ce446aa0983180bbb4775187ed6873a9e26dbc9540f623392": {
                "Name": "nginx",
                "EndpointID": "09336c1acc58f9b75ea3ced604a7d82b168a5f0cd132ae4f2fffd267a416b022",
                "MacAddress": "02:42:ac:13:00:04",
                "IPv4Address": "172.19.0.4/16", #<-これ!!
                "IPv6Address": ""
            },
            "842244244bf00593e25317dd1cafb35ae03cfbc5246a87c666c6434a6ad7540d": {
                "Name": "db",
                "EndpointID": "3810c5b1b5f87b3ba4331320fcb2106696f3dcd157a771f9de63aeffc3a14720",
                "MacAddress": "02:42:ac:13:00:02",
                "IPv4Address": "172.19.0.2/16", #<-これ!!
                "IPv6Address": ""
            },
            "c0906b3fc86d92f4b89474b7647e85a36e489469f83905c94785f2f278807532": {
                "Name": "api",
                "EndpointID": "e291176e7bd08040c2486d0409dab009a724fb7e3f0da3a5a9585cb23344a999",
                "MacAddress": "02:42:ac:13:00:03",
                "IPv4Address": "172.19.0.3/16", #<-これ!!
                "IPv6Address": ""
            }
        },
        "Options": {},
        "Labels": {}
    }
]

コンテナ間通信をするときは、コンテナ名でエンドポイントを指定することができるから便利

# db:3306 の部分 <コンテナ名:ポート>
MYSQL_CONNECTION=local:local@tcp(db:3306)/infra-challenge 

マウントについて

nginx

    volumes:
      - ./web/env/nginx:/etc/nginx/conf.d
      - ./web/src:/var/www/html
  • nginxにはdefault.confというものがあり、それを自分の設定に書き換えたいので、./web/env/nginx で書き換えてる
  • /var/www/html以下がdockerコンテナ内のnginxフォルダ構成になる(する)ので、index.htmlがあるディレクトリを同期

api

    volumes:
      - ./api:/go/src/github.com/infra-challenge/api
      - /Users/username/go:/go
  • とりあえずGo Pathが通っているところをdockerコンテナ側にマウントしてる。
  • APIのファイル等も同期

db

    volumes:
      - ./db/conf/my.cnf:/etc/mysql/conf.d/my.cnf
      - ./db/db-data:/var/lib/mysql
  • MySQLのデータの格納先が/var/lib/mysqlになるので、ホストの./db/db-dataに同期して、コンテナを除去してもデータが消えないようにする
  • 自分のDB設定、例えば、character-setとかcollationとかをマウントして上書き
# my.cnf
[mysqld]
character-set-server=utf8mb4
collation-server=utf8mb4_unicode_ci

[mysql]
default-character-set=utf8mb4

いざ、Docker Composeで起動してみる!!

コマンド

$ cd /Users/username/src/github.com/infra-challege/docker-local
$ docker network create --driver bridge local_infra_challenge_networks
[~省略~]
$ docker-compose build --no-cache
[~省略~]
$ docker-compose up -d --remove-orphans
Creating db ... done
Creating api ... done
Creating nginx ... done

$ docker ps -a
CONTAINER ID   IMAGE                                 COMMAND                  CREATED         STATUS                            PORTS                               NAMES
8fxxxb102ed4   infra-challenge:nginx-20210312        "nginx -g 'daemon of…"   4 seconds ago   Up 3 seconds                      0.0.0.0:80->80/tcp                  nginx
8fxxx25640e6   golang:1.14.7-alpine3.12              "go run main.go --ho…"   6 seconds ago   Up 4 seconds                      0.0.0.0:8080->8080/tcp              api
e5xxxe97320e   mysql:5.7                             "docker-entrypoint.s…"   6 seconds ago   Up 5 seconds (health: starting)   0.0.0.0:3306->3306/tcp, 33060/tcp   db

  • docker-composeファイルがある階層に移動
  • networks属性を指定したので、そのネットワークを作成する。
  • nginxはDockerfileをつかってimageを作成するので、ビルドする必要がある。
    • --no-cacheオプション:構築時にイメージのキャッシュを使わない。
  • docker-compose upで起動
    • -dオプション:バックグラウンド実行
    • --remove-orphansオプション:Composeファイルで定義されていないサービス用のコンテナを削除

Webページにアクセスできるか確認

(超しょぼいですが...w)
アクセスできた!
スクリーンショット 2021-03-15 11.54.07.png

user1を作成してみよう。
スクリーンショット 2021-03-15 11.57.28.png
作成できたことを確認◎
スクリーンショット 2021-03-15 11.59.15.png

コンテナ終了コマンド

$ docker-compose down
Stopping nginx ... done
Stopping api   ... done
Stopping db    ... done
Removing nginx ... done
Removing api   ... done
Removing db    ... done

止まりました。

ハマったポイント

  • go run main.goした後に、起動はしていたがポート設定がミスっていて、APIにアクセスできなかった。
    • 「なんかうまくいかないな〜〜汗」ってなったので、VSCodeから一旦APIサーバーを止めて再起動して、通信がうまくいったけど、それはコンテナ間通信ではなかった、、、
      • コンテナ上ではなくローカルでAPIサーバーが立ち上がってしまい、通信できていると勘違いしてしまった。
  • index.htmlの画面表示するポートとAPIを司るポートそれぞれが別だったので、その調整にハマった。
  • nginxには、サーバー設定が必要だった。
    • nginx.confdefault.confというファイルが存在しこれのIPアドレス、ポート等を設定しないといけないので、前もってnginxの知識をいれておかないと簡単には動かなかった
# site.conf (default.confの上書き)
server {
    listen      80;
    listen      8080; # APIで8080ポートも使ってる関係で、これも必要だった
    server_name localhost;
    root        /var/www/html;

    location / {
        index  index.html index.htm;
        root   /var/www/html;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /var/www/html/error;
    }
}

まとめ

docker-compose.ymlでコンテナを起動して、Dockerアプリケーションを作る感覚がつかめたので、楽しかったですわい!

コンテナのコントロールってすごい簡単で、サクサク動くから、せっかちな自分でも全然苦じゃない。
マスターしていきたいなああああ

以上、ありがとうございました!

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

【2021/2リリース】Go新機能embedを使ってメッセージが定義されたファイルを読み込む

はじめに

Go1.16リリース

Go1.16が2021年2月にリリースされましたね!

様々なアップデートがありましたが、主要でわかりやすい変更点は以下の通りでしょうか。

  • M1 Macに対応
  • 処理速度やメモリ使用量の改善
  • go:embedの追加

Go 1.16 adds support of 64-bit ARM architecture on macOS (also known as Apple Silicon) with GOOS=darwin, GOARCH=arm64.

For a representative set of large Go programs, linking is 20-25% faster than 1.15 and requires 5-15% less memory on average for linux/amd64, with larger improvements for other architectures and OSes. Most binaries are also smaller as a result of more aggressive symbol pruning.

The go command now supports including static files and file trees as part of the final executable, using the new //go:embed directive.

【引用】Go 1.16 Release Notes

この記事でやること

今回はこの中でもgo:embedを使って、「メッセージ一覧が定義されたjsonファイルを読み込み、そのメッセージを取得する」というモジュールの実装を解説したいと思います!

完成形

完成形だけ知りたい方向けにこちらにコードを貼っておきます。

ディレクトリ構成
(YOUR_DIRECTORY)
├─ message/
|   ├─ message.go
|   └─ messages.json
└─ main.go
messages.json
{
    "key1": "go:embed使ってみたよ!",
}
message.go
package message

import (
    // import embed
    _ "embed"
    "encoding/json"

    "github.com/labstack/gommon/log"
)

//go:embed messages.json
var msgJSON []byte

var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
    if err := json.Unmarshal(msgJSON, &msgs); err != nil {
        panic("Cannot read messages.json")
    }
}

// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
    msg, exists := msgs[key]
    if !exists {
        log.Errorf("Cannnot find this message key: %s", key)
    }
    return msg
}
main.go
func main() {
    // 一度だけ呼べばOK
    message.Read()

    // メッセージを取り出す
    fmt.Println(message.Get("key1"))
}
実行結果
$ go run .
go:embed使ってみたよ!

解説

embedとは

embedは埋め込みという意味です。
名前の通り、ファイルを変数に埋め込むことができます。

ファイルの読み込み自体は今までもできましたが、複数の書き方があったり、やや冗長な感じがありました。

【参考】[Golang] ファイル読み込みサンプル

しかし、embedを使うことにより、上記message.goのようにものの数行で書けるようになりました。

また、embedで読み込んだファイルはビルドされたバイナリにも埋め込まれます。
これには以下のようなメリットがあります。

これはGoの利点の一つである、単一の実行ファイルとしてビルドできることで、展開先の依存関係をシンプルに保つことができるという利点を強力に後押しします。設定ファイルや各種アセットをビルドに含めることで、バージョン管理やリリース作業を一層シンプルに整理できることが期待できます。

先ほど紹介した簡易WEBサーバーで例えると、WEBサーバーとコンテンツとなるHTML、CSS、Javascriptが分離している場合、ローカル環境で動いたものを実際の環境にデプロイする場合、実行バイナリと各種アセットをデプロイ対象の環境で適宜整理する必要があります。

これらを全て単一のバイナリに含めることができた場合、作業は実行バイナリを一つコピーして起動するだけになります。
新しいサーバーにデプロイする際の運用フローの整備や、プロダクション向けの構成でコンテナを構築するDockerfileを書いていく事を考えると、go:embedで極限まで簡略化できる部分が想像できるかもしれません。

【引用】Go 1.16からリリースされたgo:embedとは

コード解説

message.goの解説をしていきます。

import (
    // import embed
    _ "embed"
)

embedパッケージをimportします。

コード中でembed.~~のような使い方をしないので、ブランクインポートをします。(正確なニュアンスではないですが)

//go:embed messages.json
var msgJSON []byte

ここがembedのキモですね。
message.goと同じディレクトリにあるmessages.jsonを変数msgJSONに埋め込みます。
もちろん必ず同じディレクトリに配置する必要はなく、相対パスで指定することも可能です。

var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
    if err := json.Unmarshal(msgJSON, &msgs); err != nil {
        panic("Cannot read messages.json")
    }
}

よく見かけるコードですね。
mapのmsgsmsgJSONを代入します。

【補足】mapで定義した理由

構造体を定義しておく手法が一般的かと思いますが、messages.jsonの中身が増えるたびに構造体のプロパティを追加していくのが面倒だったのでmapに代入するようにしました。
今回はjsonファイルに文字列しか登場しておらず、map[string]stringで済むのも、mapで定義した理由の1つです。
数字など他の型も考慮する必要がある場合、map[string]interface{}にする必要があり、型安全ではなくなってしまいます。
このように複数の型を扱う場合や、ちゃんと型で守りたい場合は、構造体を使った方が良いと思います。

【参考】Go言語でJSONを扱う

// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
    msg, exists := msgs[key]
    if !exists {
        log.Errorf("Cannnot find this message key: %s", key)
    }
    return msg
}

ここはただmapからkeyでvalueを取り出しているだけです。
jsonファイルに存在しないkeyが引数で渡ってきた場合log出力するようにしています。
ちなみに存在しない場合、msgは空文字となります。

さいごに

Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!

また、BOT開発を通じてGoとLINE BOTにまとめて入門する記事をZennに掲載していますので、良かったらそちらもご覧ください!

参考

Go 1.16からリリースされたgo:embedとは

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

Go1.16新機能embedを使ってメッセージが定義されたファイルを読み込む

はじめに

Go1.16リリース

Go1.16が2021年2月にリリースされましたね!

様々なアップデートがありましたが、主要でわかりやすい変更点は以下の通りでしょうか。

  • M1 Macに対応
  • 処理速度やメモリ使用量の改善
  • go:embedの追加

Go 1.16 adds support of 64-bit ARM architecture on macOS (also known as Apple Silicon) with GOOS=darwin, GOARCH=arm64.

For a representative set of large Go programs, linking is 20-25% faster than 1.15 and requires 5-15% less memory on average for linux/amd64, with larger improvements for other architectures and OSes. Most binaries are also smaller as a result of more aggressive symbol pruning.

The go command now supports including static files and file trees as part of the final executable, using the new //go:embed directive.

【引用】Go 1.16 Release Notes

この記事でやること

今回はこの中でもgo:embedを使って、「メッセージ一覧が定義されたjsonファイルを読み込み、そのメッセージを取得する」というモジュールの実装を解説したいと思います!

完成形

完成形だけ知りたい方向けにこちらにコードを貼っておきます。

ディレクトリ構成
(YOUR_DIRECTORY)
├─ message/
|   ├─ message.go
|   └─ messages.json
└─ main.go
messages.json
{
    "key1": "go:embed使ってみたよ!",
}
message.go
package message

import (
    // import embed
    _ "embed"
    "encoding/json"

    "github.com/labstack/gommon/log"
)

//go:embed messages.json
var msgJSON []byte

var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
    if err := json.Unmarshal(msgJSON, &msgs); err != nil {
        panic("Cannot read messages.json")
    }
}

// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
    msg, exists := msgs[key]
    if !exists {
        log.Errorf("Cannnot find this message key: %s", key)
    }
    return msg
}
main.go
func main() {
    // 一度だけ呼べばOK
    message.Read()

    // メッセージを取り出す
    fmt.Println(message.Get("key1"))
}
実行結果
$ go run .
go:embed使ってみたよ!

解説

embedとは

embedは埋め込みという意味です。
名前の通り、ファイルを変数に埋め込むことができます。

ファイルの読み込み自体は今までもできましたが、複数の書き方があったり、やや冗長な感じがありました。

【参考】[Golang] ファイル読み込みサンプル

しかし、embedを使うことにより、上記message.goのようにものの数行で書けるようになりました。

また、embedで読み込んだファイルはビルドされたバイナリにも埋め込まれます。
これには以下のようなメリットがあります。

これはGoの利点の一つである、単一の実行ファイルとしてビルドできることで、展開先の依存関係をシンプルに保つことができるという利点を強力に後押しします。設定ファイルや各種アセットをビルドに含めることで、バージョン管理やリリース作業を一層シンプルに整理できることが期待できます。

先ほど紹介した簡易WEBサーバーで例えると、WEBサーバーとコンテンツとなるHTML、CSS、Javascriptが分離している場合、ローカル環境で動いたものを実際の環境にデプロイする場合、実行バイナリと各種アセットをデプロイ対象の環境で適宜整理する必要があります。

これらを全て単一のバイナリに含めることができた場合、作業は実行バイナリを一つコピーして起動するだけになります。
新しいサーバーにデプロイする際の運用フローの整備や、プロダクション向けの構成でコンテナを構築するDockerfileを書いていく事を考えると、go:embedで極限まで簡略化できる部分が想像できるかもしれません。

【引用】Go 1.16からリリースされたgo:embedとは

コード解説

message.goの解説をしていきます。

import (
    // import embed
    _ "embed"
)

embedパッケージをimportします。

コード中でembed.~~のような使い方をしないので、ブランクインポートをします。(正確なニュアンスではないですが)

//go:embed messages.json
var msgJSON []byte

ここがembedのキモですね。
message.goと同じディレクトリにあるmessages.jsonを変数msgJSONに埋め込みます。
もちろん必ず同じディレクトリに配置する必要はなく、相対パスで指定することも可能です。

var msgs map[string]string

// Read メッセージ一覧を読み込む
func Read() {
    if err := json.Unmarshal(msgJSON, &msgs); err != nil {
        panic("Cannot read messages.json")
    }
}

よく見かけるコードですね。
mapのmsgsmsgJSONを代入します。

【補足】mapで定義した理由

構造体を定義しておく手法が一般的かと思いますが、messages.jsonの中身が増えるたびに構造体のプロパティを追加していくのが面倒だったのでmapに代入するようにしました。
今回はjsonファイルに文字列しか登場しておらず、map[string]stringで済むのも、mapで定義した理由の1つです。
数字など他の型も考慮する必要がある場合、map[string]interface{}にする必要があり、型安全ではなくなってしまいます。
このように複数の型を扱う場合や、ちゃんと型で守りたい場合は、構造体を使った方が良いと思います。

【参考】Go言語でJSONを扱う

// Get keyからメッセージを取得する(keyがなければ空を返す)
func Get(key string) string {
    msg, exists := msgs[key]
    if !exists {
        log.Errorf("Cannnot find this message key: %s", key)
    }
    return msg
}

ここはただmapからkeyでvalueを取り出しているだけです。
jsonファイルに存在しないkeyが引数で渡ってきた場合log出力するようにしています。
ちなみに存在しない場合、msgは空文字となります。

さいごに

Twitterの方でも、モダンな技術習得やサービス開発の様子を発信したりしているので良かったらチェックしてみてください!

また、BOT開発を通じてGoとLINE BOTにまとめて入門する記事をZennに掲載していますので、良かったらそちらもご覧ください!

参考

Go 1.16からリリースされたgo:embedとは

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

RTPで受け取った動画をgolangで画像処理したい

の続き。

概要

動画を受け取り、動画から受け取ったフレームをある間隔で画像処理するようなアプリケーションをgolangで書きたい。

画像処理ライブラリとしてopencvを利用する必要があり、ラッパーとしてgocvを利用した。
試行錯誤した結果 opencv.OpenVideoCaptureという関数のバックエンドとしてgstreamerが使用できるという記事があり、試してみた。

構造

実際にはRTPの前にはWebRTCがあり、今回はSkyway WebRTC Gatewayを用いてWebRTC->RTPに変換している。

image.png

WebRTC周りについては別の機会に。
RTPを受け取る方法にはいろいろあると思うが、今回はopencvのgstreamer backendを使用した。この機能はgocvでは最新(0.26)でしか動作しないため、バージョンは要確認。

module version
golang 1.15.x
opencv 4.5.1
gocv 0.26.0
gstreamer 1.0.0

実装

videocapture.go

package middleware

import (
    "context"
    "fmt"
    "time"

    "github.com/pkg/errors"
    "go.uber.org/zap"
    "gocv.io/x/gocv"
)

// Handler function of VideoCaptureServer
type VideoCaptureHandler func(ctx context.Context, img *gocv.Mat) error

// VideoCaptureServer is server to handle gocv.OpenVideoCapture image
type VideoCaptureServer struct {
    handler  VideoCaptureHandler
    pipeline string
    duration time.Duration
}

func NewVideoCaptureServer(handler VideoCaptureHandler, pipeline string, duration time.Duration) *VideoCaptureServer {
    return &VideoCaptureServer{
        handler:  handler,
        pipeline: pipeline,
        duration: duration,
    }
}

// Start start server
func (s *VideoCaptureServer) Start(ctx context.Context) error {
    cap, err := gocv.OpenVideoCapture(s.pipeline)
    if err != nil {
        return errors.Wrap(err, "OpenVideoCapture failed")
    }
    defer cap.Close()

    ticker := time.NewTicker(s.duration)
    defer ticker.Stop()

    img := gocv.NewMat()

    for {
        select {
        case <-ticker.C:
            if err := s.handler(ctx, &img); err != nil {
                logger.Error("exec handler failed", zap.Error(err))
            }

        case <-ctx.Done():
            return nil
        default:
            if ok := cap.Read(&img); !ok {
                logger.Error("cap.Read failed")
            }
        }

    }
}

処理の内容はVideoCaptureHandlerとして関数インターフェースのみ定義し、動画を受け取って処理に引き渡す周りのコードをVideoCaptureServerとして定義。

videocapture_test.go

package middleware

import (
    "context"
    "fmt"
    "testing"
    "time"

    "gocv.io/x/gocv"
)

func TestVideoCaptureServer(t *testing.T) {
    ctx := context.Background()
    window := gocv.NewWindow("test")
    handler := func(ctx context.Context, img *gocv.Mat) error {
        window.IMShow(*img) // Xサーバーにテスト画像を表示
        window.WaitKey(1)
        return nil
    }
    pipeline := `udpsrc port=8888 caps="application/x-rtp,media=video,clock-rate=90000,encoding-name=H264" ! rtph264depay ! avdec_h264 ! videoconvert ! appsink`

    //pipelineを使ってOpenVideCaptureを実行し、1秒おきにhandler関数を実行
    sv := NewVideoCaptureServer(handler, pipeline, time.Second) 
    sv.Start(ctx)
}

こちらがテスト。

  • Xサーバーにテスト用画像を表示するので、動作させるためには事前にX Serverを起動しておく必要がある。
  • gst pipelineでudp port=8888で動画を受け取るように書いているが、mp4ファイルを使ったテストをしたければpipeline文字列を入れ替えるだけで良い。

評価

環境構築に難しい面はあるものの、gocv.OpenVideoCapture関数さえ使いこなせれば簡単にRTPのようなプロトコルを受け取る事が可能で、そこからの処理はほかのサーバーとそう変わらず、難しくないと感じた。

何らかの動画をgolangで受け取るような要件がある場合は、候補になると思う。

なお、pipelineの動作については、事前にgst-launchを使って確認しておくこと。appsinkを使った時のデバッグメッセージは分かりにくいと感じました・・・

https://qiita.com/kishibashi3/items/983ae9df29b6d9f8c9dd
続き

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

JWT を Go で処理するためのパッケージ

Go 言語で JWT (JSON Web Tokens) を処理したい。
以前に書いたコードを見直す機会があって調べたら状況が変化していたので記録に残しておきます。

特に記載がない限り 2021 年 3 月 14 日時点の情報になります。

調査対象

このうち実際に使ったことがあるのは dgrijalva/jwt-go と square/go-jose です。

dgrijalva/jwt-go

最終リリース: v3.2.0 (2018 年 3 月 9 日)

dgrijalva/jwt-go (fork を含む) を使用しているリポジトリの例

Go で JWT を処理する場合の定番だと思います。
古くからあるパッケージで GCP 向けの拡張 も公開されているので重宝していました。

ただし、2020 年 1 月 7 日を最後にリポジトリは更新されてません。
fork したうえで脆弱性 (CVE-2020-26160) 1 を修正したリポジトリも公開されておりこちらに切り替えているケースもあるようです。 2 3

dgrijalva/jwt-go を使用している場合は form3tech-oss/jwt-go に移行したほうがよさそうです。
I/F は変わっていないので import を書き換えるか go.mod の replace を指定すれば移行できるはずです。

square/go-jose

最終リリース: v2.5.1 (2020 年 5 月 1 日)

square/go-jose を使用しているリポジトリの例

Ed25519 や JWK (JSON Web Key) を利用したくて見つけました。

ただし、現時点では master からコードがすべて削除されています。 4 5
v3 を開発するためにリポジトリを https://github.com/go-jose/go-jose に移行したようですがこちらも更新されていないようです。

※ master は v3 を開発していたブランチですので v2 を利用している場合は影響を受けません。

lestrrat-go/jwx

最終リリース: v1.1.5 (2021 年 3 月 12 日)

dgrijalva/jwt-go や square/go-jose の Issue コメントで紹介されていました。
機能も豊富で継続的にメンテナンスされているようですが Ed25519 サポートは実験的という扱いになります。
go.mod で Go 1.15 以上を要求しているため環境によっては使用できないかもしれません。

まとめにならないまとめ

メンテナンスされなくなったことに (自分が) 全く気が付いていなかったことに驚きました。
依存パッケージ更新は Dependabot の力を借りたりしているのですがそもそも更新されていないので気が付くこともなく。

問題なく動いているように見えてもたまに既存処理を見直すことは重要かもしれません。

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