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

gobot I2CでLチカ

1.はじめに

goとgobotを使って、RaspberryPiのI2Cに接続したIOエキスパンダ(MCP23017)を操作してみます。

公式にMCP23017がサポートされてるので、チュートリアルチャレンジで終わるかと思ったら、なんと、サンプルコードが書かれてなかったので、覚え書きを兼ねて簡単にまとめてみました。

実行環境

ハードウェア Raspberry Pi 4
OS Raspbian Buster
go Ver.1.14.3
IOエキスパンダ MCP23017
→これを、RaspberryPiのI2C端子に接続済
$ uname -a
Linux raspi4 4.19.97-v7l+ #1294 SMP Thu Jan 30 13:21:14 GMT 2020 armv7l GNU/Linux
$ go version
go version go1.14.3 linux/arm
$ sudo i2cdetect -r -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:          -- -- -- -- -- -- -- -- -- -- -- -- -- 
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
20: 20 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 
70: -- -- -- -- -- -- -- --    

2.準備

インストールは別記事で解説しているので、詳細はそちらをご覧下さい。

3.LEDの点滅例

MCP23017のポートAのピン0~7につないだLEDを、流れるように点灯する例です。
こちらのドキュメントを参考12に書いてみました。

重要な関数は以下の通りです。

  • i2c.NewMCP23017Driver
i2c.WithBus(1) ラズパイのi2cは1です。(1を指定している例:$ sudo i2cdetect -r -y 1
i2c.WithAddress(0x20) MCP23017の設定にあわせます。(ここでは0x20)
  • WriteGPIO(ピン番号, 値, ポート名)
    • 値は0:消灯、1:点灯
    • ポート名は文字列で指定、"A"あるいは"B"
raspi_blink_i2c.go
package main

import (
    "time"

    "gobot.io/x/gobot"
    "gobot.io/x/gobot/drivers/i2c"
    "gobot.io/x/gobot/platforms/raspi"
)

func main() {
    //初期化
    raspiAdaptor := raspi.NewAdaptor()
    //MCP23017の初期化
    //I2Cバス1番、アドレス0x20
    i2cexp := i2c.NewMCP23017Driver(raspiAdaptor,
        i2c.WithBus(1),
        i2c.WithAddress(0x20))

    //LEDを流れるように点灯するための制御変数
    //pin: ピン番号0から開始
    //val: 点灯1状態から開始
    var pin, val uint8
    pin, val = 0, 1

    //定期実行
    work := func() {
        //100ms毎にループ
        gobot.Every(100*time.Millisecond, func() {

            //ポートAのピンを点灯/消灯
            i2cexp.WriteGPIO(pin, val, "A")

            //ピン番号を次に移動
            pin++
            if pin > 7 {
                //ピン番号7になったら、ピン番号を0に戻す
                pin = 0
                if val == 1 {
                    //現在が点灯1状態なら、次は消灯0状態に
                    val = 0
                } else {
                    //次は点灯1状態に
                    val = 1
                }
            }
        })
    }

    robot := gobot.NewRobot("bot",
        []gobot.Connection{raspiAdaptor},
        //デバイスに追加
        []gobot.Device{i2cexp},
        work,
    )

    robot.Start()
}

実行

コマンドライン
$ go run raspi_blink_i2c.go 
2020/05/31 20:56:29 Initializing connections...
2020/05/31 20:56:29 Initializing connection RaspberryPi-308BFB50 ...
2020/05/31 20:56:29 Initializing devices...
2020/05/31 20:56:29 Initializing device MCP23017-6301D4B5 ...
2020/05/31 20:56:29 Robot bot initialized.
2020/05/31 20:56:29 Starting Robot bot ...
2020/05/31 20:56:29 Starting connections...
2020/05/31 20:56:29 Starting connection RaspberryPi-308BFB50...
2020/05/31 20:56:29 Starting devices...
2020/05/31 20:56:29 Starting device MCP23017-6301D4B5...
2020/05/31 20:56:29 Starting work...
2020/05/31 20:58:30 Stopping Robot bot ...
[Ctrl-C]
$

下記の動画、下の方の8つのLEDが、左から右へ流れています。

VID_20200531_205746_144x256_fps25.gif

4.おわりに

残課題としては、[Ctrl-C]で止めたときに、IOエキスパンダを全て消灯することです。

現状のままだと、制御が止まった時点の点灯が残ってしまいます。
こちら3を参考に、SIGINT捕捉後にIOエキスパンダのピン番号全てにLを送って消灯するプログラムを試しましたが、どうやらwork := func() {の実行が生きている間に消灯処理を入れないとダメなようです。(今の所、解決できてません)

参考資料

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

aws-sdk-goを通してlocal-kmsを使う

この記事を書いた経緯

aws-sdk-goを用いた案件ソース中にkmsを用いた暗号・復号化の実装があり
kmsという概念すらわからなかった私が、ローカル環境での検証に四苦八苦した時の覚書を認めたものです

コマンドだけ知りたい人へ

local-kmsコンテナを立ち上げる

そのままdev環境・本番環境のKMSに作られている鍵を使おうとしても、IAMの認証をかけられてしまうので
権限のないローカル環境では暗号化・復号操作を行うことができません。

ローカル環境においてkmsを用いた処理をデバッグしたい場合、local-kmsというdocker imageを用いて実現することが可能です。
他コンテナとポートが重複する場合は、環境変数PORTを用いることで競合を回避できます

$ docker run -p 8080:8081 -e PORT=8081 nsmithuk/local-kms

aws configureの作成

local-kmsを使用する際も通常のインスタンスを用いる場合と同様にクレデンシャル情報が参照されますが
適当なダミーデータで問題ありません
すでに何かしらのconfigureがローカルに作成されていればスルーで問題ないと思います

$ aws configure
AWS Access Key ID [None]: dummy
AWS Secret Access Key [None]: dummy
Default region name [None]: ap-northeast-1
Default output format [None]: 

create-key コマンドによりCMKを作成する

暗号・復号化に必要な、いわゆるCMK(Customer Master Key)と呼ばれるものを作成します。
この時出力されるAccountID・KeyIDなどは適当なダミー値になります

$ aws kms create-key --endpoint-url http://localhost:8080
{
    "KeyMetadata": {
        "AWSAccountId": "111122223333",
        "KeyId": "23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
        "Arn": "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
        "CreationDate": 1590853469,
        "Enabled": true,
        "KeyUsage": "ENCRYPT_DECRYPT",
        "KeyState": "Enabled",
        "Origin": "AWS_KMS",
        "KeyManager": "CUSTOMER",
        "CustomerMasterKeySpec": "SYMMETRIC_DEFAULT",
        "EncryptionAlgorithms": [
            "SYMMETRIC_DEFAULT"
        ]
    }
}

ここで出力されたKeyIDを用いて、kms encryptを実行します

aws-sdk-goからlocal-kmsを介してkms encryptとdecryptを使う

aws-sdk-goのこちらのドキュメントに記載されているサンプルソースを拝借し、若干加筆しました。
WithEndPoint()へlocal-kmsのコンテナのアドレスを持たせ、WithDisableSSL(true)を使ってhttpへのアクセスを前提とさせたものを
aws.NewConfig()へメソッドチェーンさせることで、local-kmsを用いた暗号・復号化を可能とします

localkms.go
package main

import (
    "github.com/aws/aws-sdk-go/aws"
    "github.com/aws/aws-sdk-go/aws/session"
    "github.com/aws/aws-sdk-go/service/kms"

    "fmt"
    "os"
)

func main() {
    sess, err := session.NewSession()
    if err != nil {
        fmt.Println("aws newsession error: ", err)
        os.Exit(1)
    }

    svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("localhost:8080").WithDisableSSL(true))

    keyId := "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace"
    text := "1234567890"

    //kms encrypt
    encResult, err := svc.Encrypt(&kms.EncryptInput{
            KeyId: aws.String(keyId),
            Plaintext: []byte(text),
    })

    if err != nil {
            fmt.Println("got error encrypting data: ", err)
            os.Exit(1)
    }

    //kms decrypt
    decResult, err := svc.Decrypt(&kms.DecryptInput{
        CiphertextBlob: encResult.CiphertextBlob,
    })

    if err != nil {
        fmt.Println("got error decrypting data: ", err)
        os.Exit(1)
    }

    fmt.Println("decoded Plaintext:")
    fmt.Println(string(decResult.Plaintext)) 
}

ソースのビルド

$ go build localkms.go 

ソースの実行

$ go run localkms.go 
decoded Plaintext: 1234567890

Dockerfileでgoのソースをビルドしたい場合

このような階層の簡素なプロジェクトがあることを想定します

tree -L 3 localkms/
localkms/
├── Dockerfile
└── src
    ├── go.mod
    ├── go.sum
    ├── localkms
    ├── localkms.go
    └── vendor
        ├── github.com
        └── modules.txt

DockerFile

マルチステージビルドの書き方が望ましいですが、とりあえず簡素に作ってみます

Dockerfile
FROM golang:1.13.0

ADD ./src /go/src/local-kms
WORKDIR /go/src/local-kms
RUN cd /go/src/local-kms

ENV AWS_ACCESS_KEY_ID=dummy
ENV AWS_SECRET_ACCESS_KEY=dummy

RUN go build -mod=vendor localkms.go

ENTRYPOINT ["./localkms"]

go.mod

ファイルを作成後、go mod vendorを実行し、srcディレクトリの下にvendorディレクトリを作成します

go.mod
module src/localkms

go 1.13

require github.com/aws/aws-sdk-go v1.25.14-0.20200528180948-645efefb5bce

ソースの修正

コンテナの中からはlocalhostでlocal-kmsを参照できなくなるので
WithEndPointの記述を下記のように変更します(コンテナ名を記載することでそのコンテナのIPアドレスにリクエストが飛ぶようになります)

19行目
- svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("localhost:8080").WithDisableSSL(true))
+ svc := kms.New(sess, aws.NewConfig().WithRegion("ap-northeast-1").WithEndpoint("local-kms:8081").WithDisableSSL(true))

ソースのビルド

$ docker build ./ -t go-container

ソースの実行

local-kmsコンテナと連携するため--linkオプションを利用します

$ docker run --link local-kms:local-kms go-container
decoded Plaintext: 1234567890

ここから参考文献など

aws cliのインストール

aws kms コマンドの実行にはaws cliのインストールが必要となります
AWS CLI バージョン 1 のインストール

kmsとは?

データの暗号化に利用されるCMK(Customer Master Key)の作成と管理を容易にするマネージド型サービスです。

kms encryot

CMKのKeyIDを用いて平文を暗号化します
encryptコマンドリファレンス

暗号化されたテキストは、EncryptionAlgorithmでのエンクリプションを行った結果ががさらに
base64エンコードされて出力されます。

local-kmsを使ったencryptの一例
$ aws kms encrypt --key-id arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace --plaintext "hoge" --endpoint-url http://localhost:8080
{
    "CiphertextBlob": "S2Fybjphd3M6a21zOmV1LXdlc3QtMjoxMTExMjIyMjMzMzM6a2V5LzIzYWVlYmJkLWNiODMtNDNhMy04ODcwLWE2ZjllZTVhNGFjZQAAAADmlRx1OQCCTk5LQoIkNseoCofCRhLXo3iPpQ2lwx8E5A==",
    "KeyId": "arn:aws:kms:eu-west-2:111122223333:key/23aeebbd-cb83-43a3-8870-a6f9ee5a4ace",
    "EncryptionAlgorithm": "SYMMETRIC_DEFAULT"
}

kms decrypt

CMKにより暗号化されたCiphertextを復号します。
encryptコマンドと異なり、この時keyIDを指定する必要はありません
(ciphertextblobに含まれているメタデータからこの情報を取り込むため、明示的に指定する必要がありません)

また、この時の復号結果の出力であるPlaintextはbase64エンコードされて出てくるため
本来の平文を得るためにはbase64デコードする必要があります

local-kmsを使ったdecryptの一例
$ aws kms decrypt --ciphertext-blob fileb://<(echo 'S2Fybjphd3M6a21zOmV1LXdlc3QtMjoxMTExMjIyMjMzMzM6a2V5LzIzYWVlYmJkLWNiODMtNDNhMy04ODcwLWE2ZjllZTVhNGFjZQAAAADmlRx1OQCCTk5LQoIkNseoCofCRhLXo3iPpQ2lwx8E5A==' | base64 --decode) --endpoint-url http://localhost:8080 | jq .Plaintext --raw-output | base64 --decode
hoge

decryptコマンドリファレンス

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

git で日記を管理したい

要約

git で日記をいい感じに管理したいという思いを込めて日記管理用の cli ツールを作成いたしました。
https://github.com/komem3/go-diary

背景

バージョン管理やリモートリポジトリを使って日記・日報を管理したいという思いが有りましたが、いかんせんディレクトリが汚くなってしまうのが問題でした。定期的にディレクトリを綺麗に整理する根気があれば良いのですが、三日坊主になりやすい私なんかはそれだけで日記を書くのが嫌になってしまいます。
そこで、日記のファイルをいい感じのディレクトリ構造で管理するツールを作成しました。

インストール

go で作成したツールのため、以下のようにダウンロードします。

go get github.com/komem3/go-diary/cmd/diary

使い方

$ diary -h
Diary is a CLI libray for managing your diary.
This application can format your diary directory, and make index file.

Usage:
  diary [command]

Available Commands:
  format      Format directory
  help        Help about any command
  init        Initialize directory
  new         Generate new diary

Flags:
  -h, --help      help for diary
  -v, --version   version for diary

  Use "diary [command] --help" for more information about a command.

今回の作成したツールには以下の3つのコマンドがあるため、それぞれ解説していきます。

init

ツールをインストールした後、日記を管理したいディレクトリで最初に実行するコマンドです。
このコマンドを実行することで、他コマンドで使われるテンプレートファイルが生成されます。

$ ls
$ diary init
$ tree
tree
.
└── template
    ├── diary.template.md
    └── top.template.md

このコマンドで生成したテンプレートファイルを編集することで後程紹介するコマンドの出力を変更することができます。

new

本日の日記を生成するコマンドです。デフォルトでは本日の日付が先頭に書かれたファイルが生成されます。この時生成されるファイル名の日付情報を元にformatコマンドはフォーマットしていきます。
(記事作成日2020年5月31日)

$ diary new
$ cat 20200531.md
2020/05/31 (Sunday)

また、先程のinitコマンドで生成したテンプレートファイルを編集することで、生成されるファイルの出力内容を変更出来ます。

echo "今日やること" >> template/diary.template.md
$ diary new
$ cat 20200531.md
2020/05/31 (Sunday)
今日やること

format

日記ファイルをそのファイル名に基づいてフォルダに格納します。格納後、それぞれのファイルがどこにあるか分かりやすいようにREADME.mdファイルにリンクを書き込みます。

$ ls
20021201.md  20200101.md  20200102.txt template
$ diary format
.
├── 2002
│   └── 12
│       └── 20021201.md
├── 2020
│   ├── 01
│   │   ├── 20200101.md
│   │   └── 20200102.txt
│   └── 02
│       └── 20200202.org
├── README.md
└── template
    ├── diary.template.md
    └── top.template.md

github.com_komem3_go-diary_tree_master_sample.png

出力状態としては、実際のリポジトリを見てみるのが分かりやすいと思います。
https://github.com/komem3/go-diary/tree/master/sample

こちらも先程同様、テンプレートのファイルを変更することで好きな主力内容に変更することができます。

まとめ

git を使った日記管理を行うために、「日記の作成」「日記の整理」「日記の索引」の3つを行えるコマンドを作成いたしました。
go言語はcliツールを作り易かったのですが、日付のフォーマット指定が特殊なのがいまいちでした。そこら辺の go 特有のものや、設定の変更しやすくすることが今後の課題に思っています。

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

「Golang」 ×「Gorm」でシンプルに「Mysql」を操作する

REST APIサーバから受け取ったリクエストパラメータの内容を元にリレーショナルデータベース(Mysql)を更新する」という流れで、説明を進めています。

以下の記事の続編という形で投稿を行っておりますので、仕様などの前提条件はこちらで確認していただけるとありがたいです。

「Golang」 ×「gorilla/mux router」でシンプルなREST APIサーバーを開発する

データベース(Mysql)準備

コードを実装する前に、Mysql上でデータベースの作成を行います。
Mysql自体導入していない場合は、インストールが必要です。詳細は他のサイトで解説されているのでここでは割愛します。

# ルートユーザーでログイン
mysql -uroot

# mysql shell上
CREATE DATABASE { データベース名 };

# ログインユーザー作成
GRANT ALL PRIVILEGES ON { データベース名 }.* TO 'username'@'localhost' IDENTIFIED BY 'password';

# 一度mysqlからログアウトして、再度作成したユーザーでログイン
mysql -u username -p

# 念の為、データベースが作成されているか確認
SHOW DATABASES;

コード実装

バージョン情報

ORMマッピング

SQL文でデータベースを操作することも可能ですが、今回は「コードを簡素化する」・「単純なDB操作のみ行う」ためにORMでマッピングします。
データベースの複雑な操作が必要な場合はSQLをそのまま書いたほうが、メンテナンスしやすいと思います。
Gormを採用した理由としては「スター数が多い」「ドキュメントが充実している」からです。

root上でgoモジュールインストール
go get github.com/jinzhu/gorm

フォルダ構成

フォルダ構成
merchandise_control_system
├── config
│   └── config.go
├── controllers
│   └── webserver.go
├── models
│   └── middleware.go
├── config.ini
├── go.mod
├── go.sum
└── main.go

サーバポート情報・データベース接続情報をconfigに記載する

APIサーバのポート番号、DBの接続情報は一箇所にまとめ読み込むようにすることで可読性が向上します。

root上でgoモジュールインストール
go get gopkg.in/ini.v1
config.ini
[db]
db_driver_name = mysql
db_name = { データベース名 }
db_user_name = { データベースユーザー名 }
db_user_password = { データベースパスワード }
db_host = 127.0.0.1
db_port = 3306

[api]
server_port = 8080
config/config.go
package config

import (
    "gopkg.in/ini.v1"
    "log"
    "os"
)

type ConfigList struct {
    DbDriverName   string
    DbName         string
    DbUserName     string
    DbUserPassword string
    DbHost         string
    DbPort         string
    ServerPort     int
}

var Config ConfigList

func init() {
    cfg, err := ini.Load("config.ini")
    if err != nil {
        log.Printf("Failed to read file: %v", err)
        os.Exit(1)
    }

    Config = ConfigList{
        DbDriverName:   cfg.Section("db").Key("db_driver_name").String(),
        DbName:         cfg.Section("db").Key("db_name").String(),
        DbUserName:     cfg.Section("db").Key("db_user_name").String(),
        DbUserPassword: cfg.Section("db").Key("db_user_password").String(),
        DbHost:         cfg.Section("db").Key("db_host").String(),
        DbPort:         cfg.Section("db").Key("db_port").String(),
        ServerPort:     cfg.Section("api").Key("server_port").MustInt(),
    }
}

コード全体

イメージとしては、main.goからコントローラを呼び出し、APIサーバを起動。
APIリクエストが行われると、コントローラ内からモデルが呼び出され、データベースの値を参照・更新。
最終的にレスポンスが返されるといった流れになります。

main.go
package main

import (
    "merchandise_control_system/controllers"
)

func main() {
    controllers.StartWebServer()
}
controllers/webserver.go
package controllers

import (
    "encoding/json"
    "fmt"
    "github.com/gorilla/mux"
    "io/ioutil"
    "log"
    "merchandise_control_system/config"
    "merchandise_control_system/models"
    "net/http"
    "strconv"
)

type DeleteResponse struct {
    Id string `json:"id"`
}

func rootPage(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the Go Api Server")
    fmt.Println("Root endpoint is hooked!")
}

func fetchAllItems(w http.ResponseWriter, r *http.Request) {
    var items []models.Item
    // modelの呼び出し
    models.GetAllItems(&items)
    responseBody, err := json.Marshal(items)
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseBody)
}

func fetchSingleItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    var item models.Item
    // modelの呼び出し
    models.GetSingleItem(&item, id)
    responseBody, err := json.Marshal(item)
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseBody)
}

func createItem(w http.ResponseWriter, r *http.Request) {
    reqBody, _ := ioutil.ReadAll(r.Body)

    var item models.Item
    if err := json.Unmarshal(reqBody, &item); err != nil {
        log.Fatal(err)
    }
    // modelの呼び出し
    models.InsertItem(&item)
    responseBody, err := json.Marshal(item)
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseBody)
}

func deleteItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]

    // modelの呼び出し
    models.DeleteItem(id)
    responseBody, err := json.Marshal(DeleteResponse{Id: id})
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseBody)
}

func updateItem(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    id := vars["id"]
    reqBody, _ := ioutil.ReadAll(r.Body)

    var updateItem models.Item
    if err := json.Unmarshal(reqBody, &updateItem); err != nil {
        log.Fatal(err)
    }
    // modelの呼び出し
    models.UpdateItem(&updateItem, id)
    convertUintId, _ := strconv.ParseUint(id, 10, 64)
    updateItem.Model.ID = uint(convertUintId)
    responseBody, err := json.Marshal(updateItem)
    if err != nil {
        log.Fatal(err)
    }

    w.Header().Set("Content-Type", "application/json")
    w.Write(responseBody)
}

func StartWebServer() error {
    fmt.Println("Rest API with Mux Routers")
    router := mux.NewRouter().StrictSlash(true)

    router.HandleFunc("/", rootPage)
    router.HandleFunc("/items", fetchAllItems).Methods("GET")
    router.HandleFunc("/item/{id}", fetchSingleItem).Methods("GET")

    router.HandleFunc("/item", createItem).Methods("POST")
    router.HandleFunc("/item/{id}", deleteItem).Methods("DELETE")
    router.HandleFunc("/item/{id}", updateItem).Methods("PUT")

    return http.ListenAndServe(fmt.Sprintf(":%d", config.Config.ServerPort), router)
}

models/middleware.go
package models

import (
    "fmt"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "log"
    "merchandise_control_system/config"
    "time"
)

type Model struct {
    ID        uint        `gorm:"primary_key" json:"id"`
    CreatedAt *time.Time  `json:"created_at"`
    UpdatedAt *time.Time  `json:"updated_at"`
    DeletedAt *time.Time  `json:"deleted_at"`
}

type Item struct {
    Model
    JanCode      string     `gorm:"size:255" json:"jan_code,omitempty"`
    ItemName     string     `gorm:"size:255" json:"item_name,omitempty"`
    Price        int        `json:"price,omitempty"`
    CategoryId   int        `json:"category_id,omitempty"`
    SeriesId     int        `json:"series_id,omitempty"`
    Stock        int        `json:"stock,omitempty"`
    Discontinued bool       `json:"discontinued"`
    ReleaseDate  *time.Time `json:"release_date,omitempty"`
}

var Db *gorm.DB

func GetAllItems(items *[]Item) {
    Db.Find(&items)
}

func GetSingleItem(item *Item, key string) {
    Db.First(&item, key)
}

func InsertItem(item *Item) {
    Db.NewRecord(item)
    Db.Create(&item)
}

func DeleteItem(key string) {
    Db.Where("id = ?", key).Delete(&Item{})
}

func UpdateItem(item *Item, key string) {
    Db.Model(&item).Where("id = ?", key).Updates(
        map[string]interface{}{
            "jan_code":     item.JanCode,
            "item_name":    item.ItemName,
            "price":        item.Price,
            "category_id":  item.CategoryId,
            "series_id":    item.SeriesId,
            "stock":        item.Stock,
            "discontinued": item.Discontinued,
            "release_date": item.ReleaseDate,
        })
}

// データベースの初期化
func init() {
    var err error
    dbConnectInfo := fmt.Sprintf(
        `%s:%s@tcp(%s:%s)/%s?charset=utf8&parseTime=True&loc=Local`,
        config.Config.DbUserName,
        config.Config.DbUserPassword,
        config.Config.DbHost,
        config.Config.DbPort,
        config.Config.DbName,
    )

    // configから読み込んだ情報を元に、データベースに接続します
    Db, err = gorm.Open(config.Config.DbDriverName, dbConnectInfo)
    if err != nil {
        log.Fatalln(err)
    } else {
        fmt.Println("Successfully connect database..")
    }

    // 接続したデータベースにitemsテーブルを作成します
    Db.Set("gorm:table_options", "ENGINE = InnoDB").AutoMigrate(&Item{})
    if err != nil {
        fmt.Println(err.Error())
    } else {
        fmt.Println("Successfully created table..")
    }
}

デモ

APIサーバを起動します。と同時にデータベース接続、テーブル作成が行われます。

go run main.go

Postmanからリクエストを送ってみます。(curlコマンドでもOKです)。

【POST】 /item

最初はデータベースが空のため、データを作成します。

リクエストパラメータ
{
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:00.660666+09:00"
}
レスポンスパラメータ
{
    "id": 1,
    "created_at": "2020-05-31T13:46:09.932124+09:00",
    "updated_at": "2020-05-31T13:46:09.932124+09:00",
    "deleted_at": null,
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:00.660666+09:00"
}
DB(Mysql)
select * from items;

+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at          | updated_at          | deleted_at | jan_code       | item_name | price | category_id | series_id | stock | discontinued | release_date        |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
|  1 | 2020-05-31 13:46:10 | 2020-05-31 13:46:10 | NULL       | 32739028488888 | item_1    |  1000 |           1 |         1 |  1000 |            1 | 2020-05-31 07:00:01 |
+----+---------------------+---------------------+------------+----------------+-----------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)

【GET】 /item/1

レスポンスパラメータ
{
    "id": 1,
    "created_at": "2020-05-31T13:46:10+09:00",
    "updated_at": "2020-05-31T13:46:10+09:00",
    "deleted_at": null,
    "jan_code": "32739028488888",
    "item_name": "item_1",
    "price": 1000,
    "category_id": 1,
    "series_id": 1,
    "stock": 1000,
    "discontinued": true,
    "release_date": "2020-05-31T07:00:01+09:00"
}

【GET】 /items

レスポンスパラメータ
[
    {
        "id": 1,
        "created_at": "2020-05-31T13:46:10+09:00",
        "updated_at": "2020-05-31T13:46:10+09:00",
        "deleted_at": null,
        "jan_code": "32739028488888",
        "item_name": "item_1",
        "price": 1000,
        "category_id": 1,
        "series_id": 1,
        "stock": 1000,
        "discontinued": true,
        "release_date": "2020-05-31T07:00:01+09:00"
    }
]

【PUT】 /item/1

リクエストパラメータ
{
    "jan_code": "3273902878656",
    "item_name": "item_1_update",
    "price": 12000,
    "category_id": 1,
    "series_id": 1,
    "stock": 10,
    "discontinued": false,
    "release_date": "2020-05-30T10:30:19.978603+09:00"
}
レスポンスパラメータ
{
    "id": 1,
    "created_at": null,
    "updated_at": "2020-05-31T14:26:04.165559+09:00",
    "deleted_at": null,
    "jan_code": "3273902878656",
    "item_name": "item_1_update",
    "price": 12000,
    "category_id": 1,
    "series_id": 1,
    "stock": 10,
    "discontinued": false,
    "release_date": "2020-05-30T10:30:19.978603+09:00"
}
DB(Mysql)
select * from items;

+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
| id | created_at          | updated_at          | deleted_at | jan_code      | item_name     | price | category_id | series_id | stock | discontinued | release_date        |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
|  1 | 2020-05-31 14:25:15 | 2020-05-31 14:26:04 | NULL       | 3273902878656 | item_1_update | 12000 |           1 |         1 |    10 |            0 | 2020-05-30 10:30:20 |
+----+---------------------+---------------------+------------+---------------+---------------+-------+-------------+-----------+-------+--------------+---------------------+
1 row in set (0.00 sec)

【DELETE】 /item/1

レスポンスパラメータ
{
    "id": "1"
}

まとめ

Gormを使うと、比較的簡単にデータベース接続、テーブル作成、テーブル操作を行うことができました。
また、ORMの特徴として、DBを他のDB(例えばPostgreSQL)に変更したとしてもコードの変更が少なくて済むのもメリットかと思います。

細かいコードメンテナンスは必要ですが、一旦最低限の「APIサーバ起動」と「DB接続」が可能になりました。
今後こちらのサーバサイドコードを成長させながら、「ビュー」部分をReactNativeで実装していきたいと思います。

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

GolangのWebAPIをsupervisorでデーモン化して公開する。

GolangのWebAPIをデーモン化して公開する際に役立った情報の備忘録です。

使用環境

Amazon Linux AMI 2018.03.0 (HVM), SSD Volume Type (無料使用枠)

デーモン化とは

そもそもGolangのWebAPIを公開する方法を知らず、Linux上で"GO RUN main.go"を実行すればよいのかと考えていた。この方法だと、RLoginなどで接続を閉じたタイミングで処理も終了してしまって上手く行かなかった。

調べるうちに、デーモン(UNIX系OSにおいて、常時待機しているプログラムを指す)というものに行きついた。[Golang デーモン]で検索すると多数ヒットしたので、Golangのmain.goを常駐化させる方針を立てた。

supervisor

Golangのバイナリを常駐化させる為のツール。
Nginx同様にconfファイルで設定を行う必要がある。

supervisor.confの編集

sudo vi /etc/supervisord.conf

supervisor.confの中身

設定項目は複数あるが、基本的に[program:XX]の設定のみで動くはず。

[program:XX]
command=/home/ec2-user/hoge/main

hogeフォルダにバイナリ化したgoプログラムを配置した場合の例。
編集したら、下記の手順で再起動を行えば反映される。

supervisord.confを変更した場合の再読み込み手順

sudo supervisorctl reread

sudo supervisorctl update

sudo supervisorctl restart XX

プログラム内で環境変数を利用している場合の注意

筆者の環境だと、supervisorでデーモン化したプログラムから、環境変数がうまく読み込まれなかった。

デーモンが起動しているプロセス上の環境変数を確認する方法としては以下の通り。
①デーモンが起動しているプロセスを確認

ps ax | grep main
20591 ?        Sl     0:00 /home/ec2-user/hoge/main
20632 pts/1    S+     0:00 grep --color=auto main

②プロセス20591上での環境変数を確認

sudo strings /proc/20591/environ

出てこない場合、環境変数への追加が必要。

環境変数追加方法

.bashrcなどのファイルに直接書き込む方法
OR
supervisord.confに書き込む方法
の2通り存在する。

①~/.bashrcなどに直接書き込み

export DB_USER="sa"
export DB_Name="..."

②supervisord.confに書き込む方法
[program:XX]のenvironmentにカンマ区切りで環境変数をセットする。

[program:XX]
...
environment=DB_USER="sa",DB_Name="..."

supervisord.confを変更した場合の再読み込み手順を実行すれば、環境変数が反映されるはず。

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

【Golang】ゴルーチン②チャネル

【Golang】ゴルーチン②チャネル

Golangの基礎学習〜Webアプリケーション作成までの学習を終えたので、復習を兼ねてまとめていく。 基礎〜応用まで。

package main

//channel
//スレッド同士のデータのやり取り

//複数チャネルを作って、処理することも可能

import (
    "fmt"
)

//sからcへ送信する処理
func goroutin1(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
        //都度、計算結果を出力
        fmt.Println(sum)

    }
    //全て処理が終わったら、channelに送信
    c <- sum
}

func main() {

    /*
        //var 変数 chan データ型
        var ch chan int

        //送信専用と受信専用の型を指定すると厳密になり、異なる場合、コンパイルエラーになる。
        //送受信に制限を設けない方がオススメ。
        //受信専用チャネル
        var ch1 <-chan int
        //送信専用チャネル
        var ch2 chan<- int
    */

    /*
        チャネルの特徴

        キュー(待ち行列)の性質を持つ。
        バッファサイズは格納できる領域。
        キューにはFIFO(先入先出し)という性質がある。

        チャネルに送信
        ch <- 1
        チャネルから受信
        i := <- ch
    */

    //スライス作成
    s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    //channelを作成
    c := make(chan int)

    //並行で走らせる
    go goroutin1(s, c)
    go goroutin1(s, c)

    //channelからデータを受け取る
    //データが入るまで待ってくれる。
    //goroutine1でsumにデータが入ったらcに入り、xに送る
    x := <-c
    fmt.Println(x)
    y := <-c
    fmt.Println(y)
}

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