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

Golangでのゼロ埋め

Golangでのゼロ埋め

0~9の場合は数字の前に0を付け、
10以上の場合は0を付けない場合は以下の様に実現できる

s := 3
str := fmt.Sprintf("%02d", s)
fmt.Println(str)
// 03

s2 := 13
str2 := fmt.Sprintf("%02d", s2)
fmt.Println(str2)
// 13


知らずに以下の様な回りくどい方法で実現したので知ったときは衝撃だった...

if num < 10 {
​
    numStr = "0" + strconv.Itoa(num)
​
} else {
​
    numStr = strconv.Itoa(num)
​
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Goでスライス内の最大値・最小値を抽出する関数

はじめに

Goでコードを書いているとスライス内の最大値、もしくは最小値を算出する処理を
何度か利用したので忘れないためにもメモ

スライス内の最大値を取得

func maxInt(a []int) int {
    sort.Sort(sort.IntSlice(a))
    return a[len(a)-1]
}

スライス内の最小値を取得

func minInt(a []int) int {
    sort.Sort(sort.IntSlice(a))
    return a[0]
}

サンプルコード

以下コードをThe Go Playgroundでコピペして実行

package main
import "fmt"
import "sort"

func main() {
    test := []int{7, 8 ,1, 4, 3, 21}

    fmt.Println("max:", maxInt(test))
    fmt.Println("min:", minInt(test))
}


func maxInt(slice []int) int {
    sort.Sort(sort.IntSlice(slice))
    return slice[len(slice)-1]
}


func minInt(slice []int) int {
    sort.Sort(sort.IntSlice(slice))
    return slice[0]
}

参考

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

Go(Echo) Go Modules × Dockerで開発環境構築

はじめに

フレームワークにEcho
ライブラリ管理にgo mod
ホットリロードにfresh
を使用したGoの開発環境をDockerを使って構築したいと思います。
GoのバージョンはGo 1.15です。

この記事ではDockerのインストール方法や細かい解説等はしません。

最終的なディレクトリ構成

.
├── app
│   ├── Dockerfile
│   ├── go.mod
│   ├── go.sum
│   └── main.go
└── docker-compose.yml

GitHub

急いでいる人向け

ここからコードをダウンロードしてdocker-compose upすればできます。

Dockerfile作成

まず最初に適当なディレクトリを作成(私の場合go-dockerというディレクトリを作成)
上記のディレクトリ構成を参考にappというディレクトリを作成し、
そしてappディレクトリにDockerfileという名前でファイルを作成します。

Dockerfile
FROM golang:1.15-alpine

WORKDIR /go/src/app
ADD ./app /go/src/app

RUN apk update && \
    apk add --no-cache git && \
    go get github.com/labstack/echo/... && \
    go get github.com/pilu/fresh

EXPOSE 8080

CMD ["fresh"]

main.go作成

次はappディレクトリにmain.goというファイルを作成します。

main.go
package main

import (
    "net/http"
    "github.com/labstack/echo"
)

func main() {
    e := echo.New()
    e.GET("/", func(c echo.Context) error {
        return c.String(http.StatusOK, "Hello world")
    })
    e.Logger.Fatal(e.Start(":8080"))
}

docker-compose.yml作成

次にdocker-compose.ymlという名前でファイルを作成します。
フォルダ構成を参考にファイルを作成する場所に気をつけてください。

docker-compose.yml
version: "3"

services:
  app:
    build:
      context: .
      dockerfile: app/Dockerfile #Dockerfileの場所
    volumes:
      - ./app:/go/src/app
    ports:
      - "8080:8080"
    tty: true #コンテナ永続化

build

次にymlファイルがある場所と同じ階層でdocker-compose buildコマンドを実行します。

$ docker-compose build
Building app
Step 1/6 : FROM golang:1.15-alpine
 ---> b3bc898ad092
Step 2/6 : WORKDIR /go/src/app
 ---> Running in 55f4bfc0b0e5
Removing intermediate container 55f4bfc0b0e5
 ---> bb957624bc5e
Step 3/6 : ADD ./app /go/src/app
 ---> 94a4c0aeb52e
Step 4/6 : RUN apk update &&     apk add --no-cache git &&     go get github.com/labstack/echo/... &&     go get github.com/pilu/fresh
 ---> Running in 2e16203c8eac
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz     
v3.12.3-65-g746e0b7bc7 [http://dl-cdn.alpinelinux.org/alpine/v3.12/main]
v3.12.3-62-gebf75fec7d [http://dl-cdn.alpinelinux.org/alpine/v3.12/community]
OK: 12756 distinct packages available
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/community/x86_64/APKINDEX.tar.gz     
(1/5) Installing nghttp2-libs (1.41.0-r0)
(2/5) Installing libcurl (7.69.1-r3)
(3/5) Installing expat (2.2.9-r1)
(4/5) Installing pcre2 (10.35-r0)
(5/5) Installing git (2.26.2-r0)
Executing busybox-1.31.1-r16.trigger
OK: 22 MiB in 20 packages
Removing intermediate container 2e16203c8eac
 ---> dfceb18e2ebd
Step 5/6 : EXPOSE 8080
 ---> Running in 8353095ba65e
Removing intermediate container 8353095ba65e
 ---> fa4b48a798c4
Step 6/6 : CMD ["fresh"]
 ---> Running in a8c85cdb33ba
Removing intermediate container a8c85cdb33ba
 ---> e121e6032342

Successfully built e121e6032342
Successfully tagged go-docker_app:latest

go mod init

次にdocker-compose run --rm app go mod initというコマンドを実行します。
するとappディレクトリにgo.modが作成されます。

$ docker-compose run --rm app go mod init
Creating network "go-docker_default" with the default driver
Creating go-docker_app_run ... done
go: creating new go.mod: module app

docker-compose up

最後にddocker-compose upというコマンドを実行します。
そしてgo.sumも作られコンテナが立ち上がります。

$ docker-compose up
Creating go-docker_app_1 ... done
Attaching to go-docker_app_1
app_1  | 9:27:18 runner      | InitFolders
app_1  | 9:27:18 runner      | mkdir ./tmp
app_1  | 9:27:18 watcher     | Watching .
app_1  | 9:27:18 main        | Waiting (loop 1)...
app_1  | 9:27:18 main        | receiving first event /
app_1  | 9:27:18 main        | sleeping for 600 milliseconds
app_1  | 9:27:18 main        | flushing events
app_1  | 9:27:18 main        | Started! (5 Goroutines)
app_1  | 9:27:18 main        | remove tmp/runner-build-errors.log: no such file or directory
app_1  | 9:27:18 build       | Building...
app_1  | 9:27:34 runner      | Running...
app_1  | 9:27:34 main        | --------------------
app_1  | 9:27:34 main        | Waiting (loop 2)...
app_1  | 9:27:34 app         | 
app_1  |    ____    __
app_1  |   / __/___/ /  ___
app_1  |  / _// __/ _ \/ _ \
app_1  | /___/\__/_//_/\___/ v3.3.10-dev
app_1  | High performance, minimalist Go web framework
app_1  | https://echo.labstack.com
app_1  | ____________________________________O/_______
app_1  |                                     O\
app_1  | 9:27:34 app         | ⇨ http server started on [::]:8080

この状態になったら下記にアクセスしてHello worldと表示されていたら成功です。
http://localhost:8080/

さいごに

次はMySQLの構築とその接続を時間があればやってみたいと思います。

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

go.modの補足 パッケージ

前回の続きです。

[go.mod]
module example.com

go 1.15

require github.com/labstack/echo/v4 v4.1.17

上記のgo.modの一番上の
module example.com
の部分ですが、ここがimportして自分のパッケージを使う時の名前になります!
以下のような構成のプロジェクトの場合↓

test
 |--samplea--aaaaa.go
 |--sampleb--bbbbb.go
 |--main.go
 |--go.mod
 |--go.sum

[main.go]
package main

import(
"example.com/samplea"
"example.com/sampleb"
)

このようにして使います。
ディレクトリの名前とモジュールの名前を同じにしたければ

[go.mod]
module test

にします。

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

Go + MySQL + nginxの開発環境をDocker(docker-compose)で作る

やりたいこと

  • Go、MySQL、nginxの開発環境をDocker(docker-compose)で作る
  • Goの外部パッケージはGo Modulesで管理
  • Goのプロジェクトを実践的なものにする
  • DBのテーブル管理はマイグレーションを使う
  • testはテスト用のDBを使う

こんな人におすすめ

  • GoとMySQLでAPIサーバーの開発がしたい
  • 環境構築はDockerで手っ取り早く済ませたい
  • 拡張しやすいGoのプロジェクトが欲しい

使用するフレームワーク、バージョン

バージョン
Go 1.15
MySQL 5.7
nginx 1.19

GoのフレームワークはGin、ORMにGORMを使用。

ディレクトリ構成

├── docker-compose.yml
├── Dockerfile
├── app
│   ├── cmd
│   │   ├── migrate
│   │   │   └── main.go
│   │   └── server
│   │       └── main.go
│   ├── db
│   │   └── migrations
│   │       ├── 1_create_users.down.sql
│   │       └── 1_create_users.up.sql
│   ├── go.mod
│   ├── go.sum
│   └── pkg
│       ├── connecter
│       │   └── connecter.go
│       ├── controller
│       │   ├── router.go
│       │   └── users.go
│       └── model
│           ├── main_test.go
│           ├── user.go
│           └── user_test.go
├── mysql
│   ├── Dockerfile
│   ├── docker-entrypoint-initdb.d
│   │   └── init.sql
│   └── my.cnf
└── nginx
    ├── Dockerfile
    └── default.conf

ルートディレクトリにあるDockerfileがGoのコンテナ用です。

使い方

GitHubレポジトリはこちらにあります。
https://github.com/fuhiz/docker-go-sample

まずはdocker-compose.ymlがあるディレクトリでコンテナを立ち上げます。

$ docker-compose up -d --build

Goのコンテナに入ります。

$ docker-compose exec web bash

Goのコンテナの/appでマイグレーションを実行します。
実行されるSQLはapp/db/migrationsのファイルです。
usersとマイグレーション管理のためのschema_migrationsが作られます。

$ go run cmd/migrate/main.go -exec up

usersには名前(name)、年齢(age)、日時カラムを用意しました。

/db/migrations/1_create_users.up.sql
CREATE TABLE `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL,
  `age` int NOT NULL,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  `deleted_at` datetime,
  PRIMARY KEY (`id`)
) DEFAULT CHARSET=utf8mb4;

サーバーを起動します。

$ go run cmd/server/main.go

これでlocalhost:8082でGoのAPIにつながるようになります。

APIを使ってみる

DBにはusersテーブルがあって、ユーザーのCRUD機能が使えるようになっているので、curlで確認します。

  • ユーザー作成
$ curl localhost:8082/api/v1/users -X POST -H "Content-Type: application/json" -d '{"name": "test", "age":30}'
  • ユーザー一覧
$ curl localhost:8082/api/v1/users
{"users":[{"ID":1,"CreatedAt":"2021-01-09T11:09:31+09:00","UpdatedAt":"2021-01-09T11:09:31+09:00","DeletedAt":null,"name":"test","age":30}]}%

先ほど作ったユーザーが取得できます。

  • ユーザー更新
$ curl localhost:8082/api/v1/users/1 -X PATCH -H "Content-Type: application/json" -d '{"name": "update", "age":31}'
  • ユーザー削除
$ curl localhost:8082/api/v1/users/1 -X DELETE

docker-compose.yml

ここから環境構築の細かいところを見ていきます。

docker-compose.ymlの基本的な書き方には触れないので、参考にされる方はこちらを。
https://qiita.com/zembutsu/items/9e9d80e05e36e882caaa

それぞれのserviceについてはこのようになっております。

db

  • MySQLコンテナ
  • ユーザー名やパスワードなどを環境変数で定義。
  • docker-entrypoint-initdb.dをマウントして、コンテナ起動時にdocker-entrypoint-initdb.d/init.sqlが実行されるようにします。init.sqlでgo_sampleとgo_sample_testというデータベースを作ります。 /docker-entrypoint-initdb.dはMySQLのDockerイメージに備わっているディレクトリで初期データを作ることができます。
  • ホスト側のポートが3310なのはローカルで動かすMySQLと被らないようにするためです。
  • Sequel Proで接続するときはこうなります。

スクリーンショット 2021-01-03 11.24.13 (1).png
※パスワードはlocalpass。データベースは空でも構わないです。

web

  • Goコンテナ
  • 起動後すぐにコンテナが閉じてしまわないようにtty: trueでコンテナを永続化します。 (サーバー起動をDockerfileに書かず、コンテナの中で手動で実行するためです)
  • Goプロジェクト内で使う環境変数を定義。Goのコードでos.Getenv("DB_PASSWORD")とすればこの値が読み込めます。DB_HOSTのdbはMySQLコンテナのサービス名です。GORMでDB接続するときにこのサービス名で接続できます。
  • Goプロジェクトがある./app(ホスト)を/app(コンテナ)にマウントします。コンテナ内の/appはDockerfileのWORKDIRで指定したときに作成されます。

proxy

  • nginxはリバースプロキシによってURLを転送します。ここではhttp://localhostがGoのAPIになるように設定する目的で使います。
  • ホスト側のportは8082を指定しました。
docker-compose.yml
version: "3"

services:
  db:
    build: ./mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: localuser
      MYSQL_PASSWORD: localpass
      TZ: Asia/Tokyo
    volumes:
      - ./mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d
    ports:
      - "3310:3306"

  web:
    build: .
    tty: true
    environment:
      APP_MODE: local
      DB_PASSWORD: localpass
    volumes:
      - "./go:/app"
    depends_on:
      - db

  proxy:
    build: ./nginx
    ports:
      - 8082:80
    depends_on:
      - web

MySQLコンテナ

MySQLのDockerfileはこれらの一般的な設定です。

タイムゾーンをAsia/Tokyoにする。
設定ファイルのmy.cnfをコピーする。
起動時に実行するinit.sqlをコピーする。

mysql/Dockerfile
FROM mysql:5.7

ENV TZ Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && chown -R mysql:root /var/lib/mysql/

COPY my.cnf /etc/mysql/conf.d/my.cnf
COPY docker-entrypoint-initdb.d/init.sql /docker-entrypoint-initdb.d/

CMD ["mysqld"]

EXPOSE 3306

init.sqlではgo_sampleとテスト用のgo_sample_testを作ってユーザーの権限設定をします。

mysql/docker-entrypoint-initdb.d/init.sql
CREATE DATABASE IF NOT EXISTS `go_sample` COLLATE 'utf8mb4_general_ci' ;
CREATE DATABASE IF NOT EXISTS `go_sample_test` COLLATE 'utf8mb4_general_ci' ;

GRANT ALL ON `go_sample`.* TO 'localuser'@'%' ;
GRANT ALL ON `go_sample_test`.* TO 'localuser'@'%' ;

FLUSH PRIVILEGES ;

Goコンテナ

内容はコメントの通りで、外部パッケージをダウンロードするためにgo.modとgo.sumを事前にコピーしています。

Dockerfile
FROM golang:1.15

## 作業ディレクトリ
WORKDIR /app

# モジュール管理のファイルをコピー
COPY go/go.mod .
COPY go/go.sum .

# 外部パッケージのダウンロード
RUN go mod download

EXPOSE 9000

nginxコンテナ

nginx.confで読み込むdefault.confをコピーします。

nginx/Dockerfile
FROM nginx:1.19-alpine

COPY ./default.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
nginx/default.conf
server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://web:9000;
    }
}

nginxの設定ファイルである/etc/nginx/nginx.confで/etc/nginx/conf.d/配下の*.confを読み込むようになっているので、読み込まれる部分だけを作っています。

nginxの設定はこちらが参考になります。
https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967

大事なのはproxy_pass http://web:9000;の部分で、ここでhttp://localhosthttp://web:9000に置き換えています。
webはdocker-compose.ymlで定義したGoコンテナのサービス名です。
docker-composeはサービス間でネットワーク通信できるので、このような指定ができます。
Goプロジェクトはポートを9000でサーバーを立ち上げているのでポートはそれに合わせます。

また、docker-compose.ymlのnginxコンテナでポートを8082:80としているので、ホストからはhttp://localhost:8082でアクセスします。

ややこしいですが、とどのつまりはhttp://localhost:8082でGoのAPIが叩けることになります。

Goのプロジェクト概要

Goのコードはなるべく実践的に使えるものを意識して作りました。
ディレクトリ構造はこちらを参考にしています。
https://qiita.com/sueken/items/87093e5941bfbc09bea8

cmd
アプリケーションのエントリーポイント。
サーバー起動とマイグレーション機能を配置。

db
マイグレーションで実行したいsqlファイルを配置。

pkg
アプリケーションの挙動に関わる部分。
モデル(model)、コントローラー(controller)、接続(connecter)を作成。

マイグレーション

マイグレーション周りはこちらを参考にさせていただきました。
https://qiita.com/tanden/items/7b4fb1686a61dd5f580d

golang-migrateを使用して、db/migrationsにあるsqlファイルでDBを管理します。

ファイル名のルールは{version}を昇順にすれば、番号でもタイムスタンプでも問題ありません。
https://github.com/golang-migrate/migrate/blob/master/MIGRATIONS.md

{version}_{title}.up.{extension}
{version}_{title}.down.{extension}

ここではusersテーブルを作成する1_create_users.up.sqlとテーブル削除用の1_create_users.down.sqlを作成しました。

マイグレーション管理のファイルはcmd/migrate/main.goにあります。内容は参考サイトのほぼコピペになります。

このファイルを実行すれば追加した分の*.up.sqlだけが走ります。

$ go run cmd/migrate/main.go -exec up

戻したいときはオプションをdownにすれば、全ての*.down.sqlが実行されます。

$ go run cmd/migrate/main.go -exec down

test用のデータベースに接続したいときはAPP_MODE=testで環境変数つきで実行します。

$ APP_MODE=test go run cmd/migrate/main.go -exec up

cmd/migrate/main.goのinit()でAPP_MODEがtestなら、データベースはDB_NAME_TESTを使うようにしてます。

cmd/migrate/main.go
func init() {
    // database name decide by APP_MODE
    dbName := os.Getenv("DB_NAME")
    if os.Getenv("APP_MODE") == "test"{
        dbName = os.Getenv("DB_NAME_TEST")
    }

    Database = fmt.Sprintf("mysql://%s:%s@tcp(%s:%s)/%s",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        dbName)
}

GoのAPIの処理の流れ

エントリーポイントとなるファイルはcmd/server/main.go。
ginを使って、ポート9000でサ-バーを立ち上げています。

cmd/server/main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/controller"
)

func main() {
    // gormのDB接続
    connecter.Setup()

    router := gin.Default()

    // apiの疎通確認用
    router.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "Response OK")
    })

    // routing
    r := router.Group("/api/v1")
    controller.Setup(r)

    router.Run(":9000")
}

細かい処理は/pkgのcontrollerなどを使っていきます。

gormのDB接続はpkg/connecter/connecter.goで行います。
変数dbに*gorm.DBを格納して、DB()で呼び出せる形になっています。

接続の仕方は公式を見れば大体把握できます。
https://gorm.io/docs/connecting_to_the_database.html

データベースの各パラメータはdocker-compose.ymlで定めた環境変数から取得しています。
ここでもAPP_MODEがtestならDB_NAME_TESTを使います。

pkg/connecter/connecter.go
package connecter

import (
    "fmt"
    "os"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
)

var db *gorm.DB

func Setup() {
    // APP_MODEからデータベース名を決める
    dbName := os.Getenv("DB_NAME")
    if os.Getenv("APP_MODE") == "test"{
        dbName = os.Getenv("DB_NAME_TEST")
    }

    // DB接続 (https://gorm.io/docs/connecting_to_the_database.html)
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=true&loc=%s",
        os.Getenv("DB_USER"),
        os.Getenv("DB_PASSWORD"),
        os.Getenv("DB_HOST"),
        os.Getenv("DB_PORT"),
        dbName,
        os.Getenv("DB_LOC"))
    gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})

    if err != nil {
        panic(err)
    }

    db = gormDB
}

func DB() *gorm.DB {
    return db
}

ルーティングはpkg/controllers/router.goに書きます。
それぞれpkg/controllers/users.goのfuncを呼びます。

pkg/controllers/router.go
package controller

import (
    "github.com/gin-gonic/gin"
)

func Setup(r *gin.RouterGroup) {
    users := r.Group("/users")
    {
        u := UserController{}
        users.GET("", u.Index)
        users.GET("/:id", u.GetUser)
        users.POST("", u.CreateUser)
        users.PATCH("/:id", u.UpdateUser)
        users.DELETE("/:id", u.DeleteUser)
    }
}

pkg/controllers/users.goでは処理に応じて/pkg/model/user.goのfuncを呼びます。

pkg/controllers/users.go
package controller

import (
    "net/http"
    "strconv"

    "github.com/gin-gonic/gin"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/model"
)

type UserController struct{}

type UserParam struct {
    Name string `json:"name" binding:"required,min=1,max=50"`
    Age  int    `json:"age" binding:"required,number"`
}

// ユーザー取得
func (self *UserController) GetUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー一覧
func (self *UserController) Index(c *gin.Context) {
    users, err := model.GetUsers(connecter.DB())

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user search failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"users": users})
}

// ユーザー作成
func (self *UserController) CreateUser(c *gin.Context) {
    var param UserParam
    if err := c.BindJSON(&param); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    newUser := model.NewUser(param.Name, param.Age)
    user, err := model.CreateUser(connecter.DB(), newUser)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user create failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー更新
func (self *UserController) UpdateUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    var param UserParam
    if err := c.BindJSON(&param); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    updateParam := map[string]interface{}{
        "name": param.Name,
        "age":  param.Age,
    }

    _, err = user.Update(connecter.DB(), updateParam)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user update failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"user": user})
}

// ユーザー削除
func (self *UserController) DeleteUser(c *gin.Context) {
    ID := c.Params.ByName("id")
    userID, _ := strconv.Atoi(ID)
    user, err := model.GetUserById(connecter.DB(), userID)

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user not found"})
        return
    }

    _, err = user.Delete(connecter.DB())

    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "user delete failed"})
        return
    }

    c.JSON(http.StatusOK, gin.H{"deleted": true})
}
/pkg/model/user.go
package model

import (
    "gorm.io/gorm"
)

type User struct {
    gorm.Model
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func NewUser(name string, age int) *User {
    return &User{
        Name: name,
        Age:  age}
}

func CreateUser(db *gorm.DB, user *User) (*User, error) {
    result := db.Create(&user)

    return user, result.Error
}

func GetUsers(db *gorm.DB) ([]*User, error) {
    users := []*User{}
    result := db.Find(&users)

    return users, result.Error
}

func GetUserById(db *gorm.DB, ID int) (*User, error) {
    user := User{}
    result := db.First(&user, ID)

    return &user, result.Error
}

func (user *User) Update(db *gorm.DB, param map[string]interface{}) (*User, error) {
    result := db.Model(&user).Updates(param)

    return user, result.Error
}

func (user *User) Delete(db *gorm.DB) (*User, error) {
    result := db.Delete(&user)

    return user, result.Error
}

テスト

テストはGoのコンテナ内でAPP_MODE=testをつけて実行します。
以下手順。

マイグレーション(up)でgo_sample_testにテーブルを作成。

$ APP_MODE=test go run cmd/migrate/main.go -exec up

/pkgをテスト。

$ APP_MODE=test go test -v ./pkg/...

次のテストのためにgo_sample_testを戻す。

$ APP_MODE=test go run cmd/migrate/main.go -exec down

テストファイルは/pkg/modelにmain_test.goとuser_test.goがあります。
TestMainが最初に実行されるので、そこでDB接続しときます。

/pkg/model/main_test.go
package model_test

import (
    "testing"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
)

func TestMain(m *testing.M) {
    connecter.Setup()
    m.Run()
}

ユーザー作成のテスト。

/pkg/model/user_test.go
package model_test

import (
    "testing"

    "github.com/fuhiz/docker-go-sample/app/pkg/connecter"
    "github.com/fuhiz/docker-go-sample/app/pkg/model"
)

func TestCreateUser(t *testing.T) {
    newUser := model.NewUser("test_user", 30)
    user, _ := model.CreateUser(connecter.DB(), newUser)

    if user.Name != "test_user" {
        t.Fatal("model.CreateUser Failed")
    }
}

まとめ

ローカル環境としてはそれなりに使える環境が整えられたと思います。
自動テストやデプロイにも対応できるかは今後検証していきたいです。

Goはまだまだベストプレクティスが確立されていないようでテスト環境の切り分けは苦労しました。
改めてLaravelやRailsのような全部入りのフレームワークの偉大さも感じました。

長めの記事でしたが参考にしてもらえたらありがたいです!

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

Slack ソケットモードの最も簡単な始め方 (Go 編)

slack-go/slack がソケットモードに対応 :tada:

広く使われている Go SDK の github.com/slack-go/slack がソケットモード対応しました :tada: @mumoshu さんと @kanata2 さんの素晴らしい仕事です :clap:

https://github.com/slack-go/slack

ソケットモードアプリ起動までの手順

この記事では、Slack ソケットモードの最も簡単な始め方で使ったサンプルアプリと同じものを、この Go SDK を使って動かす方法を解説します。

プロジェクトをつくる

ここで紹介するサンプルは v0.8.0 以上で動作します。

go mod init socket-mode-app
go get github.com/slack-go/slack@v0.8.0

main.go を用意

とりあえず以下のソースコードをそのままコピペしてみてください。

package main

import (
    "fmt"
    "github.com/slack-go/slack/socketmode"
    "log"
    "os"
    "strings"

    "github.com/slack-go/slack"
    "github.com/slack-go/slack/slackevents"
)

func main() {
    webApi := slack.New(
        os.Getenv("SLACK_BOT_TOKEN"),
        slack.OptionAppLevelToken(os.Getenv("SLACK_APP_TOKEN")),
        slack.OptionDebug(true),
        slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)),
    )
    socketMode := socketmode.New(
        webApi,
        socketmode.OptionDebug(true),
        socketmode.OptionLog(log.New(os.Stdout, "sm: ", log.Lshortfile|log.LstdFlags)),
    )
    authTest, authTestErr := webApi.AuthTest()
    if authTestErr != nil {
        fmt.Fprintf(os.Stderr, "SLACK_BOT_TOKEN is invalid: %v\n", authTestErr)
        os.Exit(1)
    }
    selfUserId := authTest.UserID

    go func() {
        for envelope := range socketMode.Events {
            switch envelope.Type {
            case socketmode.EventTypeEventsAPI:
                // イベント API のハンドリング

                // 3 秒以内にとりあえず ack
                socketMode.Ack(*envelope.Request)

                eventPayload, _ := envelope.Data.(slackevents.EventsAPIEvent)
                switch eventPayload.Type {
                case slackevents.CallbackEvent:
                    switch event := eventPayload.InnerEvent.Data.(type) {
                    case *slackevents.MessageEvent:
                        if event.User != selfUserId && strings.Contains(event.Text, "こんにちは") {
                            _, _, err := webApi.PostMessage(
                                event.Channel,
                                slack.MsgOptionText(
                                    fmt.Sprintf(":wave: こんにちは <@%v> さん!", event.User),
                                    false,
                                ),
                            )
                            if err != nil {
                                log.Printf("Failed to reply: %v", err)
                            }
                        }
                    default:
                        socketMode.Debugf("Skipped: %v", event)
                    }
                default:
                    socketMode.Debugf("unsupported Events API eventPayload received")
                }
            case socketmode.EventTypeInteractive:
                // ショートカットのハンドリングとモーダル起動
                payload, _ := envelope.Data.(slack.InteractionCallback)
                switch payload.Type {
                case slack.InteractionTypeShortcut:
                    if payload.CallbackID == "socket-mode-shortcut" {
                        socketMode.Ack(*envelope.Request)
                        modalView := slack.ModalViewRequest{
                            Type:       "modal",
                            CallbackID: "modal-id",
                            Title: slack.NewTextBlockObject(
                                "plain_text",
                                "タスク登録",
                                false,
                                false,
                            ),
                            Submit: slack.NewTextBlockObject(
                                "plain_text",
                                "送信",
                                false,
                                false,
                            ),
                            Close: slack.NewTextBlockObject(
                                "plain_text",
                                "キャンセル",
                                false,
                                false,
                            ),
                            Blocks: slack.Blocks{
                                BlockSet: []slack.Block{
                                    slack.NewInputBlock(
                                        "input-task",
                                        slack.NewTextBlockObject(
                                            "plain_text",
                                            "タスク",
                                            false,
                                            false,
                                        ),
                                        // multiline is not yet supported
                                        slack.NewPlainTextInputBlockElement(
                                            slack.NewTextBlockObject(
                                                "plain_text",
                                                "タスクの詳細・期限などを書いてください",
                                                false,
                                                false,
                                            ),
                                            "input",
                                        ),
                                    ),
                                },
                            },
                        }
                        resp, err := webApi.OpenView(payload.TriggerID, modalView)
                        if err != nil {
                            log.Printf("Failed to opemn a modal: %v", err)
                        }
                        socketMode.Debugf("views.open response: %v", resp)
                    }
                case slack.InteractionTypeViewSubmission:
                    // モーダルからの送信をハンドリング
                    if payload.CallbackID == "modal-id" {
                        socketMode.Debugf("Submitted Data: %v", payload.View.State.Values)
                        socketMode.Ack(*envelope.Request)
                    }
                default:
                    socketMode.Debugf("Skipped: %v", payload)
                }

            default:
                socketMode.Debugf("Skipped: %v", envelope.Type)
            }
        }
    }()

    socketMode.Run()
}

起動する

前の記事と同様、環境変数を設定した上で起動してみましょう。

export SLACK_APP_TOKEN=xapp-<自分のトークンの値>
export SLACK_BOT_TOKEN=xoxb-<自分のトークンの値>
go run main.go

以下のようなメッセージが表示されていれば、接続できています!

$ go run main.go
api: 2021/01/19 15:41:48 slack.go:125: Challenging auth...
sm: 2021/01/19 15:41:48 socket_mode_managed_conn.go:241: Starting SocketMode
sm: 2021/01/19 15:41:48 main.go:133: Skipped: connecting
api: 2021/01/19 15:41:48 socket_mode.go:30: Using URL: wss://wss-primary.slack.com/link/?ticket=xxx&app_id=yyy
sm: 2021/01/19 15:41:48 socket_mode_managed_conn.go:249: Dialing to websocket on url wss://wss-primary.slack.com/link/?ticket=xxx&app_id=yyy
sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:78: WebSocket connection succeeded on try 0
sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:422: Starting to receive message
sm: 2021/01/19 15:41:49 main.go:133: Skipped: connected
sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:464: Incoming WebSocket message: {
  "type": "hello",
  "num_connections": 1,
  "debug_info": {
    "host": "applink-xxx-yyy",
    "build_number": 10,
    "approximate_connection_time": 18060
  },
  "connection_info": {
    "app_id": "A111"
  }
}

sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:476: Finished to receive message
sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:422: Starting to receive message
sm: 2021/01/19 15:41:49 socket_mode_managed_conn.go:319: Received WebSocket message: {"type":"hello","num_connections":1,"debug_info":{"host":"applink-xxx-yyy","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}
sm: 2021/01/19 15:41:49 main.go:133: Skipped: hello
sm: 2021/01/19 15:41:51 socket_mode_managed_conn.go:544: WebSocket ping message received: Ping from applink-xxx-yyy

次のステップ

オフィシャルのサンプルコードは以下の場所にありますので、チェックしてみてください。

https://github.com/slack-go/slack/tree/master/examples/socketmode

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