20200315のGoに関する記事は13件です。

【Golang】Go 1.14での独自パッケージimportでエラーになるとき

数年Goを離れてたら、バージョン1.14になってたのでキャッチアップしなければ(Go 1.10の人)。
go modによる管理が楽しい。

TL;DR

  • 各ディレクトリにgo.modを設置
    • go mod initをする
  • local importするため各go.modファイルにreplaceを追加
    • 参照階層がネストするときに注意
      • 例)main.go → ./api.go → ./domain/models/Sample.go
      • mainのgo.modにはreplace文が2行必要になります
        • main.goから見たapiの相対パス
        • main.goから見たmodelsの相対パス

出てきて困ったエラー

C:\Users\username\go\src\github.com\username\repo>go build ./...
go: finding module for package github.com/username/repo/domain/models
api\api.go:13:2: no matching versions for query "latest"

とか

go: github.com/username/repo/api@v0.0.0-00010101000000-000000000000 requires
        github.com/username/repo/domain/models@v0.0.0-00010101000000-000000000000: invalid version: unknown revision 000000000000

前提

go envの結果です。

env.log
set GO111MODULE=on
set GOPATH=C:\Users\username\go

ディレクトリ構成

%GOPATH%配下である必要はないですが、一応標準的な配置。本記事に関係ないファイルを除外して記載してあります。

tree.log
%GOPATH%\src\github.com\username\repo
│  main.go
│
├─api
│      api.go
│
└─domain
   └─models
          Sample.go

解決手順

./main.go→./api/api.go

main.goのなかで、自作apiパッケージを利用する場合、下記の手順が必要。

  • main.goと同階層にあるgo.modにreplace追加
    • replace github.com/username/repo/api => ./api
  • apiの階層にgo.mod作成
    • cd ./api
    • go mod init
  • main.goにimport
    • import api "github.com/username/repo/api"

./api/api.go→./domain/models/Sample.go

api.goのなかで、自作modelsパッケージを利用する場合、下記の手順が必要。

  • api.goと同階層にあるgo.modにreplace追加
    • replace github.com/username/repo/domain/models => ../domain/models
    • そのgo.modから見ての相対パスなので注意!
  • domain/modelsの階層にgo.mod作成
    • cd ./domain/models
    • go mod init
  • api.goにimport
    • import models "github.com/username/repo/domain/models"
  • main.goと同階層にあるgo.modにもreplace追加
    • replace github.com/username/repo/domain/models => ./domain/models

mainのgo.mod

go.mod
module github.com/username/repo

go 1.14

replace (
    github.com/username/repo/api => ./api
    github.com/username/repo/domain/models => ./domain/models
)

require (
    ...
)

apiのgo.mod

go.mod
module github.com/username/repo/api

go 1.14

replace github.com/username/repo/domain/models => ../domain/models

require (
    ...
)

解決後のgo.mod分布イメージ

tree.log
%GOPATH%\src\github.com\username\repo
│  go.mod
│  go.sum
│  main.go
│
├─api
│      api.go
│      go.mod
│      go.sum
│
└─domain
   └─models
          go.mod
          Sample.go

今回は問題なかったけど

  • 将来domain直下に.goファイルが出てくるようになったら、今回と同様の手順で解決できるはず
  • domain/repositoriesとか作ってrepositoriesパッケージ使う場合も同じお話

同ジャンルのQiitaの先人たち

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

interface をなるべく変更しないように gorm を使う

背景

clean architectureでアプリケーションを作っていると db とのデータのやりとりは抽象化しておくことが多いと思います。そのときに定義する interface が findUserByXXX, findByUserZZZ だと他のデータの取り出し方をしたいと思った時に interface を新規に追加しないといけません。メソッドを分けたとしても、似たようなコードがその中に書かれ、コードベースは肥大化していきます。
そこで、クエリビルダっぽいものを作ってなるべくデータの取り出し方を抽象化したメソッド内部でやるのではなく、クエリビルダに任せたいと思いました。
試作的に書いたのでまだ考慮は必要なのですが、簡単なCRUDアプリケーションのときには十分使えそうなので自ら使って改良を加えて行きたいです。

内容

db とのやり取りを Repository を使って実装します。
Repository は Query struct を受け取り、struct に設定された条件をデータ取得時に組み立て、クエリを実行します。そのため、Query struct はビジネスロジック側で条件設定を行いRepository側にただ渡すだけにしました。本当は domain や model の層にinterface や User struct を置きたいのですが簡略化して書いています。あくまでサンプルコードです。

repo.go
type User struct {
    ID        uint      `json:"-" gorm:"primary_key"`
    CreatedAt time.Time `json:"createdAt" gorm:"index"`
    UpdatedAt time.Time `json:"updatedAt" gorm:"index"`
    Email     string    `json:"email" gorm:"unique_index"`
    Name      string    `json:"name" gorm:"name"`
}

type Repo interface {
    GetUser(q *Query) (User, error)
}

type repo struct {
    db *gorm.DB
}

func (a *repo) GetUser(q *Query) (User, error) {
    var u User
    db := q.build(a.db)
    err := db.Take(&u).Error
    return u, err
}

クエリはこんな風にひとまず書きました。基本的に、Repository側のメソッドで build() を呼んでもらえればあとは実行するだけです。

query.go
package gormq

import (
    "time"

    "github.com/jinzhu/gorm"
)

type Query struct {
    SelectFields []string
    Conditions   []func(db *gorm.DB) *gorm.DB
    Preloads     []func(db *gorm.DB) *gorm.DB
    Order        func(db *gorm.DB) *gorm.DB
    ForUpdate    func(db *gorm.DB) *gorm.DB
}

func NewQuery(f []string) *Query {
    if len(f) == 0 {
        f = []string{"*"}
    }
    return &Query{
        SelectFields: f,
    }
}

func (a *Query) AddWhere(cond string, v interface{}) {
    a.Conditions = append(a.Conditions, func(db *gorm.DB) *gorm.DB {
        return db.Where(cond, v)
    })
}

func (a *Query) AddPreload(target string) {
    a.Preloads = append(a.Preloads, func(db *gorm.DB) *gorm.DB {
        return db.Preload(target)
    })
}

func (a *Query) AddOr(cond string, v interface{}) {
    a.Conditions = append(a.Conditions, func(db *gorm.DB) *gorm.DB {
        return db.Or(cond, v)
    })
}

func (a *Query) SetOrder(cond string) {
    a.Order = func(db *gorm.DB) *gorm.DB {
        return db.Order(cond)
    }
}

func (a *Query) EnableForUpdate() {
    a.ForUpdate = func(db *gorm.DB) *gorm.DB {
        return db.Set("gorm:query_option", "FOR UPDATE")
    }
}

func (a *Query) build(db *gorm.DB) *gorm.DB {
    db = db.Select(a.SelectFields)
    for _, item := range a.Conditions {
        db = item(db)
    }
    for _, item := range a.Preloads {
        db = item(db)
    }
    if a.Order != nil {
        db = a.Order(db)
    }
    if a.ForUpdate != nil {
        db = a.ForUpdate(db)
    }

    return db
}

しかし、色々な部分で build() が呼ばれるため小さな変更にも気を使う必要があるかもしれません。そんな時のために、データの取り出し方について最低限テストを書いておくべきかと思います。もちろん、Query 側の SQL 発行のテストもしておきたいですよね。go-sql-mock を使えば簡単にできます。

query_test.go
package gormq

import (
    "database/sql/driver"
    "reflect"
    "testing"

    "github.com/DATA-DOG/go-sqlmock"
    "github.com/jinzhu/gorm"
)

func emptyConn() *gorm.DB {
    db, _, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
    gdb, _ := gorm.Open("mysql", db)
    // gdb.LogMode(true)
    return gdb
}

func TestRepo_GetUser(t *testing.T) {
    type fields struct {
        db func() *gorm.DB
    }
    type args struct {
        q func() *Query
    }
    tests := []struct {
        name    string
        fields  fields
        args    args
        want    User
        wantErr bool
    }{
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT * FROM `users` LIMIT 1").WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{})
                    return q
                },
            },
        },
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT * FROM `users`  WHERE (name = ?) AND (email = ?) LIMIT 1").WithArgs(driver.Value("test"), driver.Value("test@gmail.com")).WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{})
                    q.AddWhere("name = ?", "test")
                    q.AddWhere("email = ?", "test@gmail.com")
                    return q
                },
            },
        },
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT * FROM `users`  WHERE (name IN (?,?)) LIMIT 1").WithArgs(driver.Value("1"), driver.Value("2")).WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{})
                    q.AddWhere("name IN (?)", []string{"1", "2"})
                    return q
                },
            },
        },
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT id, name FROM `users`  WHERE (name IN (?)) OR (email = ?) LIMIT 1").WithArgs(driver.Value("1"), driver.Value("test@com")).WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{"id, name"})
                    q.AddWhere("name IN (?)", []string{"1"})
                    q.AddOr("email = ?", "test@com")
                    return q
                },
            },
        },
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT id, name FROM `users` LIMIT 1").WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{"id, name"})
                    q.AddPreload("Profile")
                    return q
                },
            },
        },
        {
            fields: fields{
                db: func() *gorm.DB {
                    db, mock, _ := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
                    mock.ExpectQuery("SELECT id, name FROM `users` ORDER BY id desc LIMIT 1").WillReturnRows(sqlmock.NewRows([]string{}))
                    gdb, _ := gorm.Open("mysql", db)
                    return gdb
                },
            },
            args: args{
                q: func() *Query {
                    q := NewQuery([]string{"id, name"})
                    q.SetOrder("id desc")
                    return q
                },
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            a := &Repo{
                db: tt.fields.db(),
            }
            got, err := a.GetUser(tt.args.q())
            if (err != nil) != tt.wantErr {
                if err != gorm.ErrRecordNotFound {
                    t.Errorf("Repo.GetUser() error = %v, wantErr %v", err, tt.wantErr)
                    return
                }
            }
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("Repo.GetUser() = %v, want %v", got, tt.want)
            }
        })
    }
}

参考

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

Golangでwebアップロードダーを作る

とりあえず作成したので、アップロード
https://github.com/karosuwindam/goupload

重要部分を抜き出し
アップロードの関数部分

upload.go
package main
import (
    "fmt"
    "net/http"
    "os"

)
func upload(w http.ResponseWriter, r *http.Request) {
    var data []byte = make([]byte, 1024)
    var tmplength int64 = 0
    var output string
    urldata := ""
    searchdata := ""
    if r.Method == "POST" {
        file, fileHeader, e := r.FormFile("file")
        if e != nil {
            fmt.Fprintf(w, "%v", backHtmlUpload())
            return
        }
        writefilename := fileHeader.Filename
        fp, err := os.Create(UPLOAD + "/" + writefilename)
        if err != nil {
        }
        defer fp.Close()
        defer file.Close()
        for {
            n, e := file.Read(data)
            if n == 0 {
                break
            }
            if e != nil {
                return
            }
            fp.WriteAt(data, tmplength)
            tmplength += int64(n)
        }
        fmt.Printf("POST\n")
    } else {
        fmt.Printf("GET\n")
    }
    fmt.Fprintf(w, "ファイルアップロード")
}

サーバのスタート部分

main.go
package main

import (
    "net/http"
    "fmt"
)

const Port = "8080"
const Ipdata = ""


func webstart() {
    http.HandleFunc("/upload/", upload)
    http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("./html"))))
    http.ListenAndServe(Ipdata+":"+Port, nil)
}

func main() {
    fmt.Printf("%v:%v Webserver start\n", Ipdata, Port)
    webstart()
}

index部分ファイルアップロードはブラウザのPOST命令に任せる

index.html
<html>
    <head>
        <title>upload</title>
    </head>
    <body>
        <form action="/upload/" method="post" enctype="multipart/form-data">
        <input type="file" name="file" id="up_loadfile" multiple="multiple">
        <input type="submit" value="send">
        </form>
    </body>
</html>

メモ書き程度なので、githubのアップロードファイルをビルドした方が多機能かも

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

Golangでwebアップローダーを作る

とりあえず作成したので、アップロード
https://github.com/karosuwindam/goupload

重要部分を抜き出し
アップロードの関数部分

upload.go
package main
import (
    "fmt"
    "net/http"
    "os"

)

const UPLOAD = "upload"

func upload(w http.ResponseWriter, r *http.Request) {
    var data []byte = make([]byte, 1024)
    var tmplength int64 = 0
    var output string
    urldata := ""
    searchdata := ""
    if r.Method == "POST" {
        file, fileHeader, e := r.FormFile("file")
        if e != nil {
            fmt.Fprintf(w, "アップロードに失敗しました。")
            return
        }
        writefilename := fileHeader.Filename
        fp, err := os.Create(UPLOAD + "/" + writefilename)
        if err != nil {
        }
        defer fp.Close()
        defer file.Close()
        for {
            n, e := file.Read(data)
            if n == 0 {
                break
            }
            if e != nil {
                return
            }
            fp.WriteAt(data, tmplength)
            tmplength += int64(n)
        }
        fmt.Printf("POST\n")
    } else {
        fmt.Printf("GET\n")
                fmt.Fprintf(w, "GET読み込み")
        return
    }
    fmt.Fprintf(w, "ファイルアップロード")
}

サーバのスタート部分

main.go
package main

import (
    "net/http"
    "fmt"
)

const Port = "8080"
const Ipdata = ""


func webstart() {
    http.HandleFunc("/upload/", upload)
    http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("./html"))))
    http.ListenAndServe(Ipdata+":"+Port, nil)
}

func main() {
    fmt.Printf("%v:%v Webserver start\n", Ipdata, Port)
    webstart()
}

index部分ファイルアップロードはブラウザのPOST命令に任せる

index.html
<html>
    <head>
        <title>upload</title>
    </head>
    <body>
        <form action="/upload/" method="post" enctype="multipart/form-data">
        <input type="file" name="file" id="up_loadfile" multiple="multiple">
        <input type="submit" value="send">
        </form>
    </body>
</html>

メモ書き程度なので、githubのアップロードファイルをビルドした方が多機能かも

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

「Azure SDK for Go」とは?

Azure SDK for Goは、マイクロソフト社のパブリッククラウド「Azure」をGoでコーディングして扱うためのSDK(ソフトウェア開発キット)です。

Apache License 2.0ライセンスのオープンソースソフトウェアであり、GitHub上で管理および開発されています。

最初のコミットは2014年8月11日で、2016年、2017年とベータ期間を経て、2018年2月12日のv14.0.0で正式リリースとなりました。

おおよそ毎月末にメジャーバージョンのアップデートが行われます。

現在では、日本語ドキュメントも公開され、サポートされるAzureサービスも増えています。

当時、他のプログラミング言語のSDKやドキュメントが出揃っていた中、Goだけがなかなか正式リリースされずヤキモキしたり、ベータ期間中にストレージ系のパッケージが本家から出たり戻ったりしていたのを眺めていたも良い思い出です。

まずは「Azure/azure-sdk-for-go」の README.md や「Go 開発者向けの Azure」のドキュメントを読み進め、どんなものなのか?何ができるのか?を見て行きます。

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

コマンドラインから文字列の取得

初めに

Atcoderの文字列取得の定石を貼ります。

コマンドラインから複数行一度に取得

package main

import(
   "fmt"
   "bufio"
   "stdin"
)

func main(){
  lines := getStdin()
  for k,v := range lines{
    fmt.Printf("line[%s]=%s\n",k,v)
  }

}

func getStdin() []string {
    stdin := bufio.NewScanner(os.Stdin)
    lines := []string{}
    for stdin.Scan() {
        if err := stdin.Err(); err != nil {
            fmt.Fprintln(os.Stderr, err)
        }
        lines = append(lines, stdin.Text())
    }
    return lines
}

実行結果

$go run sample.go
a b c d e//コマンドライン入力
f g h i j//コマンドライン入力
line[0]="a b c d e"//出力
line[1]="f g h i j"//出力

コマンドラインから得た文字列を空白区切りで返す(string型)

func splitspace(line string) []string {
    words := strings.Split(line, " ")
    return words
}

コマンドラインから得た文字列を空白区切りで返す(int型)

func splitspace(line string) []int {
    buf := strings.Split(line, " ")
    num := stringToInt(buf)
    return t
}

func stringToInt(i []string) []int {
    f := make([]int, len(i))
    for n := range i {
        f[n], _ = strconv.Atoi(i[n])
    }
    return f
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

(主に)Goの開発環境構築でWindowsユーザがMacに歩み寄る工夫

はじめに

チームの開発環境の構築手順って、みなさまどうしていますか?多くのチームはリポジトリ直下のREADMEや、チームWikiに用意することが多いのではないでしょうか?

その際、そのリポジトリ固有のBuild & Deploy & Release手順は記載することは普通だと思いますが、ここで問題になるのは、開発者の環境がWindows, Macなどで割れている場合です。全員Windows or Macで揃えろよってことですが、今の自分のチームはMac:Windows=7:3くらいです。

Windowsは参画したてのメンバーや、アルバイトer社員の方が多く、事実上スキルが低いメンバーが多いのですが、環境構築手順がMacに比重高めなので結構翻訳が大変な場合があります。ここでなるべくWindowsユーザがMacユーザ用に書かれた環境構築手順書でも対応できるようなTipsをまとめていきたいと思います。

1. 環境構築の設定

WindowsとMacで揺れているところです。

  • Windows: set AWS_REGION=ap-northeast-1
  • Mac: export AWS_REGION=ap-northeast-1

毎回、exportをsetと入力してもらうのは大変ですが、Windowsでも export のコマンドで環境変数を設定できます。

doskey export=set $*

上記を実行すると、export を setのエイリアスのように認識してくれます。
これを永続化するためには以下のサイトにあるように、macroファイルを準備し、ショートカットの引数に追加すると良いようです。

https://ricecake362865159.wordpress.com/2019/05/16/cmd%E3%81%A7%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9%E7%9A%84%E3%81%AA%E3%82%82%E3%81%AE%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B/

2. Makeコマンド

特にGoだとMakefileでビルドスクリプトを用意する文化があるので、Windowsユーザはちょっと対応が大変です。Makeコマンド自体は、Windowsでも追加でインストールできるものの、その内部でgrepコマンドなどを利用されたり、shellスクリプトを実行されるとお手上げです。とはいえ、このMakefileをWindows用も用意するのはダブルメンテですし、MacユーザはWindows側の実行テストするのがハードルが高いので、即座に腐っていきそうです。バッドスメルです?。

というわけで、WSL一択になります。この時点で1の環境変数設定もWSL側でやったほうが良いという結論になりがちですが、環境構築手順のレベル感を見てどこからWSLを使うかは個別判断になると思います。

Windows標準のコマンドプロンプトから、 wsl または bash と入力するとコンソールがそのまま切り替わり、 exit を打つとWindows側に戻れるので便利です。

ワンラインで処理したい場合は bash コマンドだと以下のように実行できます。

bash -c "ls -la /c/mnt/Users/laqiiz/go/src | grep github"

参考: https://docs.microsoft.com/ja-jp/windows/wsl/interop

というわけで、Makeコマンドを実行したい場合は以下のように実行できます。

bash -c "make"

3. GOPATHの設定

2で動けば良いですが、おそらく課題として、内部で go build する時にWindows側とWSL側でGOPATH が異なるのでうまく動かないことも多いと思います。少しややこしいことになりました。対応としてはオススメは Windows側に WSL側の GOPATH を寄せることです。

bash # WSL側へ移動
export GOPATH=/mnt/c/Users/<ユーザ名>/go
exit # Windows側へ戻る

これで go build も問題なく動くと思います。

しかし、go generate系も動かない可能性があるので、PATHにも追加します

export PATH=${PATH}:${GOPATH}/bin

Windows側で go get -u などでインストールした実行ファイルは .exe なのでそのまま使えませんので、WSL側でも実行ファイルを作成する必要がありますが、MakefileにInstall手順も書かれている場合は問題にならないと思います。

4. GoアプリのBuild

Goはクロスコンパイルできますので、Windowsでも問題なくLinux用の実行ファイルを作成できます。
しかし、環境手順には以下のようなワンラインで書かれていることも多いのではないでしょうか?

ワンラインで書かれて辛いケース
GOOS=linux GOARCH=amd64 go build ./cmd/your-app/your-app.go

この場合、Windowsに翻訳すると以下のように分割する必要があります。

Windowsだとこれ
set GOOS=linux
set GOARCH=amd64
go build ./cmd/your-app/your-app.go

コレでも良いのですが、このまま go test すると内部でbuildされる実行ファイルがWindowsで実行できなくなるため、 set GOOS=windows と設定し直す必要があり非常にノイジーです。

というわけで、これもWSL側で実行するのがオススメです。

bash -c "GOOS=linux GOARCH=amd64 go build ./cmd/your-app/your-app.go"

4. AWSCLIのための設定

Goのよくある使い所の一つとして、サーバサイドのWebAPI開発があると思います。このときAWSにDeployする方も多いのではないでしょうか? AWSCLIはWindowsでもMacでも公式で準備されているため、何も問題が無いことが多いですがいくつかのAWSコマンド実行する上で、ややこしいことがあります。

AWS CLIの設定

Profileを利用している場合、Windows側とWSL側でダブルメンテになるのは、設定漏れで作業効率を落とす原因になる事が多いので、なるべく避けた方が良いと思います。

例によって、WSL側の設定をWindows側に寄せます。

ln -s /mnt/c/Users/<User-Name>/.aws ~/.aws

これでAWSCLIなどで利用する configやcredentialsをWindows側と共用できるようになりました。もちろん、credentialsは最上位の機密情報ですので、取り扱いはWindows側と同様に注意して取り扱いましょう。

Lambdaのデプロイ

LambdaでGoアプリをデプロイするときは、以下のようにZIP化する必要があります。

Lambdaのデプロイ
GOOS=linux GOARCH=amd64 go build ./cmd/lambda/lambda.go

# ZIPファイルを作成
zip -j lambda.zip lambda

# Lambdaのコードデプロイ(Lambda関数自体はすでに作成されている前提)
aws --profile <YOUR_ENV> lambda update-function-code --function-name <YOUR_LAMBDA_NAME> --zip-file fileb://lambda.zip

この、 zip コマンドはWindowsに無いのですが、公式には build-lambda-zip ツールのインストールが推奨されています。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-go-how-to-create-deployment-package.html

これでももちろん良いですが、微妙にWindows側とコマンドが異なりややこしいです。ここでもWSLの出番となります。
複数ラインの場合は bash -c "コマンド" と渡すのではなく、bash + exit で切り替えがオススメです。

bash # Bashを起動
GOOS=linux GOARCH=amd64 go build ./cmd/lambda/lambda.go

zip -j lambda.zip lambda

aws --profile <YOUR_ENV> lambda update-function-code --function-name <YOUR_LAMBDA_NAME> --zip-file fileb://lambda.zip

exit # (必要に応じて)Windows側へ戻る

Docker

DockerもVolume周りの設定をされていると、微妙にWindowsでうまく動かない可能性があります。Docker Desktop for WSL 2でお試し中です。うまくいき次第追記予定です(2020/03/15)

まとめ

  • 環境変数設定くらいであればdoskeyで逃げられる
  • それ以上になれば、WSLをうまく利用し逃げるのが楽。その際の設定周りはなるべくWindowsと共用し、ダブルメンテにならないようにするのがオススメ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWSを用いたGoの開発環境構築でWindowsユーザがMacユーザに歩み寄るナレッジをまとめた

はじめに

チームの開発環境の構築手順って、みなさまどうしていますか?多くのチームはリポジトリ内のREADMEや、チームWikiに準備することが多いのではないでしょうか?

その際、そのリポジトリ固有のBuild & Deploy & Release手順は記載することは普通だと思いますが、ここで問題になるのは、開発者の環境がWindows, Macなどで割れている場合です。全員Windows or Macで揃えろよってことですが、今の自分のチームはMac:Windows=7:3くらいです。ハードやライセンスの問題から自分たちと同じ用に完全に開発環境を揃えるのも難しいチームも多いでしょう。Amazon Workspacesは一つの解には間違いないですが、なるべく既存の資産を有効活用したいPJも多いと思います。

私のチームの話ですがWindowsは参画したてのメンバーや、アルバイトer社員の方が多く、事実上スキルが低いメンバーが多いのですが、環境構築手順がMacに比重高めなので脳内翻訳が大変な場合があります。

ここではなるべくWindowsユーザがMacユーザ用に書かれた環境構築手順書でも対応できるようなTipsをまとめていきたいと思います。

1. 環境構築の設定

WindowsとMacで揺れやすいところです。

  • Windows: set AWS_REGION=ap-northeast-1
  • Mac: export AWS_REGION=ap-northeast-1

毎回、exportをsetと入力してもらうのは大変ですが、ここで朗報です。Windowsでも export のコマンドで環境変数を設定できます(!)

doskey export=set $*

上記を実行すると、export を setのエイリアスのように認識してくれます。
これを永続化するためには以下のサイトにあるように、macroファイルを準備し、ショートカットの引数に追加すると良いようです。

https://ricecake362865159.wordpress.com/2019/05/16/cmd%E3%81%A7%E3%82%A8%E3%82%A4%E3%83%AA%E3%82%A2%E3%82%B9%E7%9A%84%E3%81%AA%E3%82%82%E3%81%AE%E3%82%92%E3%81%A4%E3%81%8F%E3%82%8B/

2. パスの指定

自分たちのチームでGo系だとよくあるのが、 cd ${GOPATH}/src/github.com/future-architect/<Repository> みたいな指定を書かれることです。

Windowsだと %GOPATH% と書き換える以外なく、早くも脱落感があります。次の3,4,5のようにWSLをうまく活用しましょう。

3. Makeコマンド

特にGoだとMakefileでビルドスクリプトを用意する文化があるので、Windowsユーザはちょっと対応が大変です。Makeコマンド自体は、Windowsでも追加でインストールできるものの、その内部でgrepコマンドなどを利用されたり、shellスクリプトを実行されるとお手上げです。とはいえ、このMakefileをWindows用も用意するのはダブルメンテですし、MacユーザはWindows側の実行テストするのがハードルが高いので、即座に腐っていきそうです。バッドスメルです?。

というわけで、WSL一択になります。この時点で1の環境変数設定もWSL側でやったほうが良いという結論になりがちですが、環境構築手順のレベル感を見てどこからWSLを使うかは個別判断になると思います。

Windows標準のコマンドプロンプトから、 wsl または bash と入力するとコンソールがそのまま切り替わり、 exit を打つとWindows側に戻れるので便利です。

ワンラインで処理したい場合は bash コマンドだと以下のように実行できます。

bash -c "ls -la /c/mnt/Users/laqiiz/go/src | grep github"

というわけで、Makeコマンドを実行したい場合は以下のように実行できます。

bash -c "make"

WSLを利用したワンラインでの実行方法は以下で詳しく説明されていておすすめです。

https://docs.microsoft.com/ja-jp/windows/wsl/interop

4. GOPATHの設定

2で動けば良いですが、おそらく課題として、内部で go build する時にWindows側とWSL側でGOPATH が異なるのでうまく動かないことも多いと思います。少しややこしいことになりました。対応としてはオススメは Windows側に WSL側の GOPATH を寄せることです。

bash # WSL側へ移動
export GOPATH=/mnt/c/Users/<ユーザ名>/go
exit # Windows側へ戻る

これで go build も問題なく動くと思います。

しかし、go generate系も動かない可能性があるので、PATHにも追加します

export PATH=${PATH}:${GOPATH}/bin

Windows側で go get -u などでインストールした実行ファイルは .exe なのでそのまま使えませんので、WSL側でも実行ファイルを作成する必要がありますが、MakefileにInstall手順も書かれている場合は問題にならないと思います。

5. GoアプリのBuild

Goはクロスコンパイルできますので、Windowsでも問題なくLinux用の実行ファイルを作成できます。
しかし、環境手順には以下のようなワンラインで書かれていることも多いのではないでしょうか?

ワンラインで書かれて辛いケース
GOOS=linux GOARCH=amd64 go build ./cmd/your-app/your-app.go

この場合、Windowsに翻訳すると以下のように分割する必要があります。

Windowsだとこれ
set GOOS=linux
set GOARCH=amd64
go build ./cmd/your-app/your-app.go

コレでも良いのですが、このまま go test すると内部でbuildされる実行ファイルがWindowsで実行できなくなるため、 set GOOS=windows と設定し直す必要があり非常にノイジーです。

というわけで、これもWSL側で実行するのがオススメです。

bash -c "GOOS=linux GOARCH=amd64 go build ./cmd/your-app/your-app.go"

6. AWSCLIのための設定

Goのよくある使い所の一つとして、サーバサイドのWebAPI開発があると思います。このときAWSにDeployする方も多いのではないでしょうか? AWSCLIはWindowsでもMacでも公式で準備されているため、何も問題が無いことが多いですがいくつかのAWSコマンド実行する上で、ややこしいことがあります。

AWS CLIの設定

Profileを利用している場合、Windows側とWSL側でダブルメンテになるのは、設定漏れで作業効率を落とす原因になる事が多いので、なるべく避けた方が良いと思います。

例によって、WSL側の設定をWindows側に寄せます。

ln -s /mnt/c/Users/<User-Name>/.aws ~/.aws

これでAWSCLIなどで利用する configやcredentialsをWindows側と共用できるようになりました。もちろん、credentialsは最上位の機密情報ですので、取り扱いはWindows側と同様に注意して取り扱いましょう。

Lambdaのデプロイ

LambdaでGoアプリをデプロイするときは、以下のようにZIP化する必要があります。

Lambdaのデプロイ
GOOS=linux GOARCH=amd64 go build ./cmd/lambda/lambda.go

# ZIPファイルを作成
zip -j lambda.zip lambda

# Lambdaのコードデプロイ(Lambda関数自体はすでに作成されている前提)
aws --profile <YOUR_ENV> lambda update-function-code --function-name <YOUR_LAMBDA_NAME> --zip-file fileb://lambda.zip

この、 zip コマンドはWindowsに無いのですが、公式には build-lambda-zip ツールのインストールが推奨されています。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-go-how-to-create-deployment-package.html

これでももちろん良いですが、微妙にWindows側とコマンドが異なりややこしいです。ここでもWSLの出番となります。
複数ラインの場合は bash -c "コマンド" と渡すのではなく、bash + exit で切り替えがオススメです。

bash # Bashを起動
GOOS=linux GOARCH=amd64 go build ./cmd/lambda/lambda.go

zip -j lambda.zip lambda

aws --profile <YOUR_ENV> lambda update-function-code --function-name <YOUR_LAMBDA_NAME> --zip-file fileb://lambda.zip

exit # (必要に応じて)Windows側へ戻る

7. Docker

DockerもVolume周りの設定をされていると、微妙にWindowsでうまく動かない可能性があります。Docker Desktop for WSL 2でお試し中です。うまくいき次第追記予定です(2020/03/15)

設定について

説明の簡略化のため、今回はコンソール内でexportコマンドで済ましていますが、GoやAWSCLIを毎回利用する場合はWSL側の ~/.profile などに記載して永続化することをオススメします。

まとめ

  • 環境変数設定くらいであればdoskeyで逃げられる
  • それ以上になれば、WSLをうまく利用し逃げるのが楽。その際の設定周りはなるべくWindowsと共用し、ダブルメンテにならないようにするのがオススメ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[golang]gooseでrake db:migrateみたいな事をやる

Go言語でマイグレーションどうやんの…というところで色々試したうちの1つ。
Railsのrake db:migrationみたいな事ができるので、Railsに慣れている人には比較的使いやすいのではないかと思う。

環境

go 1.13.4

gooseが記事執筆時点で対応しているデータベース

  • postgres
  • mysql
  • sqlite3
  • mssql
  • redshift

gooseのインストール

go get -u github.com/pressly/goose/cmd/goose

各種コマンド

databaseを作る

db:create相当のコマンドは無い

マイグレーションファイルを作る

$ goose mysql "user:password@/testdb?parseTime=true" create AddColumnToTestTable sql
2020/03/15 11:10:29 Created new file: 20200315111029_AddColumnToTestTable.sql

すると以下のようなファイルが出力されるので、中身のSQL文は自分で書く。
Railsほどいい感じにテンプレート出力されるわけではない。

-- +goose Up
-- SQL in this section is executed when the migration is applied.

-- +goose Down
-- SQL in this section is executed when the migration is rolled back.

マイグレーションを実行する

-- +goose Up セクションに記載された内容が実行される。

# 1つだけ実行
goose mysql "user:password@/testdb?parseTime=true" up-by-one

# まだ実行してないものを全部実行
goose mysql "user:password@/testdb?parseTime=true" up

# 特定のバージョンまで実行
# そのバージョンも含む
goose mysql "user:password@/testdb?parseTime=true" up-to 20200315111029

巻き戻し

-- +goose Down セクションに記載された内容が実行される。

# 最初まで巻き戻し
goose mysql "user:password@/testdb?parseTime=true" down

# 特定のバージョンまで巻き戻し
# 指定したバージョンは含まない
goose mysql "user:password@/testdb?parseTime=true" down-to 20200315111029

再実行

特定のマイグレーションをdownしてからupする。

goose mysql "user:password@/testdb?parseTime=true" redo 20200315111029

欠点

コマンドラインにパスワードを含むのでDBのパスワードがコマンド履歴に残る。
BitBucketのほうのGooseは設定ファイルに書けるらしい。

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

[golang]rake db:migrateみたいな事をgoでできるgooseを使ってみた

Go言語でマイグレーションどうやんの…というところで色々試したうちの1つ。
Railsのrake db:migrationみたいな事ができるので、Railsに慣れている人には比較的使いやすいのではないかと思う。

環境

go 1.13.4

gooseが記事執筆時点で対応しているデータベース

  • postgres
  • mysql
  • sqlite3
  • mssql
  • redshift

gooseのインストール

go get -u github.com/pressly/goose/cmd/goose

各種コマンド

databaseを作る

db:create相当のコマンドは無い

マイグレーションファイルを作る

$ goose mysql "user:password@/testdb?parseTime=true" create AddColumnToTestTable sql
2020/03/15 11:10:29 Created new file: 20200315111029_AddColumnToTestTable.sql

すると以下のようなファイルが出力されるので、中身のSQL文は自分で書く。
Railsほどいい感じにテンプレート出力されるわけではない。

-- +goose Up
-- SQL in this section is executed when the migration is applied.

-- +goose Down
-- SQL in this section is executed when the migration is rolled back.

マイグレーションを実行する

-- +goose Up セクションに記載された内容が実行される。

# 1つだけ実行
goose mysql "user:password@/testdb?parseTime=true" up-by-one

# まだ実行してないものを全部実行
goose mysql "user:password@/testdb?parseTime=true" up

# 特定のバージョンまで実行
# そのバージョンも含む
goose mysql "user:password@/testdb?parseTime=true" up-to 20200315111029

巻き戻し

-- +goose Down セクションに記載された内容が実行される。

# 最初まで巻き戻し
goose mysql "user:password@/testdb?parseTime=true" down

# 特定のバージョンまで巻き戻し
# 指定したバージョンは含まない
goose mysql "user:password@/testdb?parseTime=true" down-to 20200315111029

再実行

特定のマイグレーションをdownしてからupする。

goose mysql "user:password@/testdb?parseTime=true" redo 20200315111029

欠点

コマンドラインにパスワードを含むのでDBのパスワードがコマンド履歴に残る。
BitBucketのほうのGooseは設定ファイルに書けるらしい。

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

【Go】Mac + VSCode + Go 環境設定

Goのインストール


基本こちらに記載されていた手順でインストール
参考リンク

【エラー対応諸々】


① Go:Install/Update Tools が FAILED になる (スクショないので参考画面)

→以前GoLand(IDE)で旧バージョン(v1.11)を利用していたため。
→旧バージョンを削除し新バージョン(v1.13)をインストール (アップグレードの手順参考)
(旧バージョンのアンインストールは /usr/local/go 消したら良いよう)

→アップグレード後、FAILEDなくすべてSUCCESSした
→VSCodeにて問題なくHello world もデバッグ実行できることを確認

② godoc command not found

go get golang.org/x/tools/cmd/godoc で godocのダウンロードしたが
godoc fmt で確認すると godoc command not found となった

(旧バージョン&GoLandでうまく動いていたから、これまで手動でPATH設定していなかった)

→vim ~/.bash_profile で
GOPATH を明記
PATH に GOPATH と GOROOT を明記

スクリーンショット 2020-03-14 15.22.53.png

source ~/.bash_profile で反映

再度 godoc fmt すると、

Unexpected arguments. Use "go doc" for command-line help output instead. For example, "go doc fmt.Printf".

と表示され、

"go doc fmt.Printf" すると確認できた。

参考リンク諸々
http://horie1024.hatenablog.com/entry/2014/08/24/024733
http://kodama-tech.hatenablog.com/entry/2016/12/14/002115
https://stackoverflow.com/questions/34708207/command-not-found-go-on-mac-after-installing-go

③ コードにコメント書かないとエラー表示される

golint_err.png

→VSCodeの基本設定にてgolintを検索し、Lint on Save を OFF にして無効にした(VSCode再起動)

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

【Go】【xorm/reverse】MySQLのINT UNSIGNEDに対応する!

はいどうも~:clap:

新人のプルリクに、選択肢を2つ与えつつ「自分のチカラで考えてみそ」とレビューしたら、なぜか新米部長から横レスで「じゃあお前が決めろよ、ちなみに選択肢Bは俺的に無しだから」と来て、何が琴線?に触れたのか不明ですがとりまLGTMで済ませた昨今、いかがお過ごしでしょうか:sunglasses::v:

今回は、↓でご紹介したxorm/reverseを改造したお話です。

何を変えたの?

MySQL DBリバース

Go言語のstructコード

INT UNSIGNEDカラムが、ただのint型structフィールドになってしまう

uint型になってほしい

改造

変更点

まずは、最新版xorm/reversexorm/xormの帳尻合わせ

xorm/reversexorm/xorm(xorm本体)をインポートしていて大部分依存しているのですが、xorm/xormのバージョンが0.810.82と上がっていたので「どうせなら最新版に合わすか」と軽い気持ちで

go.mod
require (
・
・
・
    xorm.io/xorm latest
)

latestで引っ張りなおしたら、結構影響が…
このver0.01の更新でリファクタリングがあったようです。

ので、まずはxorm/reverseを最新xorm/xormに合わせる修正をします。

※ご注意: giteaリポジトリ上のルートはxormですが、go package名は公式サイトドメインに沿ってxorm.ioになっているようです。

cmd/reverse.go, language/golang.go, language/language.go

Table, Column構造体がパッケージcoreからschemasに移動していたので、

  • import "xorm.io/xorm/schemas" を追加
  • core.Table, core.Columnschemas.Table, schemas.Column に修正

これで、最新版xorm/xormをインポートしたxorm/reverseがビルド可能になりました。

リバースロジックに手を入れ…

そう単純にはいかないようです…

処理を追跡すると、language/golang.gotypestring()で呼ばれているschemas.SQLType2Type()の中で変換が行われているようです。
これってxorm/xorm本体側のfuncですよね…

…分かりました、やりましょう!
xorm/xormも改造しちゃいます!

xorm/xorm最新版をgit clone

早速xorm/xormgit cloneしてきます。
ディレクトリはxorm.io/reverseと同じ階層になるようにします。

$ cd ../
$ git clone https://gitea.com/xorm/xorm.git
$ ls
xorm/ reverse/

手元のxorm/reverseが、同じく手元のxorm/xormを参照するように変更

※Go Modules方式でビルドしている前提です。

reverse/go.modに↓を追記します。

go.mod
replace xorm.io/xorm => ../xorm

これでxorm.io/xormパッケージのライブラリだけ、ネット上に公開されているものではなく手元のディスク内のものを参照するようになります。

今度こそ、リバースロジックに手を入れる!

えっと、目論見を付けたのはschemas.Type2SQLType()でしたね。
このfuncは…xorm/schemas/type.goにありますね。
早速手を入れていきましょう。

type.go
// default sql type change to go types
func SQLType2Type(st SQLType) reflect.Type {
    name := strings.ToUpper(st.Name)
    switch name {
    case Bit, TinyInt, SmallInt, MediumInt, Int, Integer, Serial:
        return reflect.TypeOf(1)
    case BigInt, BigSerial:
        return reflect.TypeOf(int64(1))

ここにcase UInt:を足せばいいのかなと思いますが、きっとそれだけでは済まないですね…
でもこれはこれで必要なので足します。

type.go
func SQLType2Type(st SQLType) reflect.Type {
    name := strings.ToUpper(st.Name)
    switch name {

    case UInt: // ←
        return reflect.TypeOf(uint(1)) // ←

定数宣言部が冒頭にあるのでそれも足します。

type.go
var (

    UInt       = "INT UNSIGNED" // ←

    SqlTypes = map[string]int{

        UInt:       NUMERIC_TYPE, // ←

)

さて、GoLandのデバッグ実行を使ってみましたがINT UNSIGNEDカラムを持つテーブルを食わせてもSQLType2Typeのパラメータst.Nameとして渡ってくる時点でUNSIGNEDが削られてしまっています…

…ここはひとつ、ちゃんとイチから辿ってみますか。

  • reverse/cmd/reverse.go
    • Reverse() >
    • runreverse()>
  • xorm/engine.go
    • DBMetas()>
    • loadTableInfo() >
  • xorm/dialects/dialect.go
    • GetColumns() >
  • xorm/dialects/mysql.go
    • GetColumns()

お、ここにMySQLのカラム宣言部を解析する処理がありました。

mysql.go
func (db *mysql) GetColumns(ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) {

        cts := strings.Split(colType, "(")
        colName := cts[0]
        colType = strings.ToUpper(colTypeBase)

ここで、例えばINT(10) UNIQUE UNSIGNED NOT NULLといったカラム宣言からINTを切り出しています。
これだと確かにUNSIGNEDが切り捨てられちゃいますね。
なのでINT(X) ... UNSIGNED ...からINT UNSIGNEDとして拾い上げるよう修正します。

mysql.go
func (db *mysql) GetColumns(ctx context.Context, tableName string) ([]string, map[string]*schemas.Column, error) {

        cts := strings.Split(colType, "(")
        //colName := cts[0]
        var colTypeBase string
        group :=  regexp.MustCompile(`(?i)([a-zA-Z]+).*(\sUNSIGNED?).*`).FindSubmatch([]byte(colType))
        if group == nil {
            colTypeBase = cts[0]
        } else {
            for i, g := range group {
                if i == 0 {
                    continue
                }
                colTypeBase += string(g)
            }
        }
        colType = strings.ToUpper(colTypeBase)

ここ、実はMySQL側のバージョンによって罠がありました。

MySQL5.xだとINT UNSIGNED ...とカラム宣言しても、テーブル作成時、自動的にINT(10) UNSIGNED ...と、(10)が付けられてしまうので↑この修正が必要です。(MySQL8.xだとこの現象は発生しません。)

いずれにしろ、↑のようにしておけば大丈夫です。

実行

ではxorm/reverseを実行してみましょう。

$ cd reverse
$ go run main.go -f example/my-mysql.yml

※設定ファイルmy-mysql.ymlの内容は、前回記事 https://qiita.com/yagrush/items/cc60166d85befbcbf3d6 をご参照下さい。

すると、例えば↓のテーブルが

CREATE TABLE `users` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(32) COLLATE utf8_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
);

type User struct {
    Id        uint      
    Name      string    
    CreatedAt *time.Time
}

Id uintになってますね:ok_hand:

おわり

ということで、以下を修正することでINT UNSIGNEDカラムに対応することができました。

  • xorm/reverse

    • cmd/reverse.go
    • language/golang.go
    • language/language.go
  • xorm/xorm

    • schemas/type.go
    • dialects/mysql.go

実際には、この修正でBIGINT UNSIGNEDなどもUNSIGNED付きで拾ってしまうようになるので 併せて修正が必要なのですが、ほぼコピペで済むので。
記事が冗長になってしまうので、ここでは割愛します。

それではまた!

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

golang,docker,mysqlの環境をherokuにデプロイする

はじめに

言語: golang
コンテナ: docker-compose
RDB: mysql
ORM: gorm
マイグレーション: migrate

な環境をherokuにデプロイするまで結構ハマったので残す。

コード

https://github.com/pokotyan/study-slack

実行したherokuのコマンド一覧

$ cd /hoge/huga                    # アプリケーションのコードがあるところに移動
$ heroku container:login           # ログイン
$ heroku create -a app_name        # herokuアプリの作成
$ heroku git:remote -a app_name    # herokuリポジトリをgit登録
$ heroku addons:add cleardb:ignite # mysqlのアドオンを追加
$ heroku config                    # CLEARDB_DATABASE_URLが登録されていることを確認
$ heroku config:set DATABASE_URL="<ユーザー名>:<password>@tcp(<ホスト名>:3306)/<DB名>?parseTime=true" # CLEARDB_DATABASE_URLの値を元にsql.Open()に渡す用の文字列に整形
$ heroku config                    # DATABASE_URLが登録されていることを確認
$ heroku stack:set container       # heroku.ymlを使う時はこれがいるぽい
$ git push heroku master           # リリース

heroku.yml

heroku.ymlを使うとCI/CDみたいなことができる。アプリのルートディレクトリに置いて使う。
buildにはdockerのビルドの指定ができる。
releaseにはリリースする際に挟みたい処理があれば書くことができる。ここではマイグレーションの実行をしている。
runはプロセスタイプ1ごとに実行するコマンドを指定する。

./heroku.yml
build:
  docker:
    web: Dockerfile
    worker:
      dockerfile: Dockerfile
      target: builder 
release:
  image: worker
  command:
    - make up_migrate_prod
run:
  web: /main

Dockerfile

上述のheroku.ymlが参照するDockerfile。アプリのルートディレクトリに置く。
いくつかポイントがある。

./Dockerfile
FROM golang:alpine as builder

RUN apk update \
  && apk add --no-cache git curl make gcc g++ \
  && go get github.com/oxequa/realize

WORKDIR /app
COPY go.mod .
COPY go.sum .

RUN go mod download
COPY . .

RUN GOOS=linux GOARCH=amd64 go build -o /main

FROM alpine:3.9

COPY --from=builder /main .

ENV PORT=${PORT}
ENTRYPOINT ["/main"]

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

./Dockerfile
RUN apk update \
  && apk add --no-cache git curl make gcc g++ \
  && go get github.com/oxequa/realize

realizeは開発時のホットリロードのため。
make、gcc、g++はheroku.ymlのreleaseフェーズにてmakeコマンドでマイグレーションを流せるようにするため
curlはherokuのUI上でログを残すため(※)。

※ curlを入れていないとこんな感じで何も表示されない。リリースが途中で死んでもなんで落ちたかが追えなくなるので入れておいた方がいいと思う。ログを出すためにcurlが必要なことは公式にも記載されている。
スクリーンショット 2020-03-15 0.59.01.png

ビルド

builderのイメージはheroku.ymlでイメージのビルドをする際に使ったり、マイグレーションを実行する時のイメージとして利用している。

./Dockerfile
FROM golang:alpine as builder
./heroku.yml
build:
  docker:
    web: Dockerfile
    worker:
      dockerfile: Dockerfile
      target: builder # builderのイメージをbuildする際に使う
release:
  image: worker # 上記のworkerのイメージをreleaseフェーズでも使う
  command:
    - make up_migrate_prod

アプリの実行

RUN GOOS=linux GOARCH=amd64 go build -o /main でビルドしたファイルを実行する

./Dockerfile
FROM alpine:3.9

COPY --from=builder /main .

ENV PORT=${PORT}
ENTRYPOINT ["/main"]
./heroku.yml
run:
  web: /main

docker-compose.yml

ローカルで開発する時のみに利用するdocker-compose.yml。
mysqlのコンテナが立ち上がる際に docker-entrypoint-initdb.d を利用して CREATE DATABASE をするようにしている。

herokuの本番環境ではdatabaseは heroku addons:add cleardb:ignite で用意されたものを利用する。
そのため、本番環境ではこのdocker-compose.ymlは利用しない。

dockers/docker-compose.yml
version: "3.5"

services:
  mysql:
    container_name: push_study_db
    image: mysql:5.7.22
    volumes:
      - ./mysql/:/docker-entrypoint-initdb.d/
      - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf
    environment:
      - MYSQL_ALLOW_EMPTY_PASSWORD=yes
    ports:
      - 4306:3306
  app:
    build:
      context: ..
      target: builder
    volumes:
      - ../:/app
    command: realize start --server
    environment:
      - API_VERSION=development
    ports:
      - 7777:7777
    depends_on:
      - mysql

起動するポート

herokuはアプリが起動するたびにポートが変わるらしい。$PORTを指定して起動するようにする。

    router.Run(":" + os.Getenv("PORT"))

CLEARDB_DATABASE_URL

mysqlのアドオンを追加するとCLEARDB_DATABASE_URLという環境変数が自動で設定される。
heroku.ymlのreleaseフェーズで流れるようにしたマイグレーションだが、そのコードでは以下のようにしてdbと接続していた。

    dbURL := os.Getenv("CLEARDB_DATABASE_URL")
    db, _ := sql.Open("mysql", dbURL)

リリースを実行すると invalid memory address or nil pointer dereference のエラーが出る。

スクリーンショット_2020-03-15_1_17_03.png

結果として、herokuが自動で作成してくれるCLEARDB_DATABASE_URLの書式をsql.Openが求めている書式に変換する必要があった。こちらの記事を参考にさせていただきました。
冒頭のherokuのコマンド一覧のところでも記載しているが、"<ユーザー名>:<password>@tcp(<ホスト名>:3306)/<DB名>?parseTime=true" の形にしてあげる必要があった。

最後に

herokuのリリース方法、色々ありすぎてまとまった情報を見つけるのが難しい。

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