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

【Go言語】EchoとGORMでREST API(CRUD)を実装する

こちらに移動しました。 https://nishinatoshiharu.com/restapi-echo-gorm/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GO言語基礎①

Go言語のメリット・デメリット メリット 文法がシンプルでわかりやすい ライブラリが豊富なため簡潔に書くことができる 処理速度が速い 直接機械語に変換するためコンパイルが早い 並列処理を簡潔に書くことができる。goroutineやchannelを利用することで大量データの並列処理が可能 シングルバイナリ、クロスコンパイルにより環境依存を減らすことができる デメリット コードの継承がない(enableにより似たような機能はある) →詳細は以下記事を参照 Go言語で「embedded で継承ができる」と思わないほうがいいのはなぜか? Genericsがない →使えるようになったらしい Goに導入されるGenericsについてまとめてみた 例外がない(エラーは値として処理) javaの場合以下のようにエラーを飛ばしますが throw new Exception(...) goでは以下のような記載になります。 // 何かの処理の戻り値にエラーが存在する場合 if err != nil { // ログ出力で処理終了 log.Fatal(err) } Javaとの違い クラスではなく構造体で処理を紐付ける クラスで行いたい処理としては以下のことがメインとなります。 処理毎によるメソッドの切り分け 処理に必要な関数、変数、定数の定義 Go言語ではstructを用いて行います。 package main import ( "fmt" ) type Human struct { name string age int } func (h Human) SayName() string { return h.name + "です。" } func main() { h := Human{"Mike", 10} fmt.Println(h, h.name, h.age) fmt.Println(h.SayName()) } 実行結果 {Mike 10} Mike 10 Mikeです。 上記ではHumanというstructを利用してクラス変数を定義しています。 Humanにたいしてあたいを入れることにより、その変数、クラス関数を利用することができます。 継承が存在しない(似たような機能Embeddedは存在する) goにおいて継承は存在しません。その代わりにEmbeddedは存在します。 厳密にはクラスと違う部分もあるのですが、気になる場合は下記記事を参考にしてください。 Go言語で「embedded で継承ができる」と思わないほうがいいのはなぜか? 今回はHumanを親クラス、Japaneseを子クラスとして記述します。 package main import ( "fmt" "strconv" ) type Human struct { name string age int } type Japanese struct { Human weight int } func (h Human) SayName() string { return h.name + "です。" } func (h Human) SayAge() string { return strconv.Itoa(h.age) + "です。" } func (j Japanese) SayName() string { return "初めまして" + j.name + "です。" } func New(name string, age int, weight int) *Japanese { return &Japanese{Human{name, age}, weight} } func main() { // 初期化 j := New("Mike", 20, 55) // 親の関数を呼び出せていること fmt.Println(j.SayAge()) // オーバーライドされていること fmt.Println(j.SayName()) } 実行結果 20です。 初めましてMikeです。 Human,JapaneseにSayName関数を実装してオーバーライドされるか、親クラスの関数の呼び出しが可能か確認しました。 実行結果の通り親クラスの関数の呼び出し、オーバーライドが行えていることがわかります。 Embeddedにより、オブジェクト指向の特徴である。継承、ポリモーフィズムといった機能が実装できることがわかります。 基本文法 パッケージ Goではプログラムをpackageごとに分割しています。 importすることで他パッケージの利用もできます。 Goでは大文字で始まるものがPublic、この字で始まるものがPrivateとされるため、大文字始まりの定数や関数を呼び出すことができます。 package main import ( "fmt" "strconv" ) func main() { fmt.Println("Hello!"); } function(ファンクション):関数 Goではファンクションに処理を書きます。Javaでいうメソッドです。 大文字であればPublic、小文字であればPrivateで利用できます。 以下のように定義されます。 func <関数名>([引数]) [戻り値の型] { [関数の本体] } func (j Japanese) SayName() string { return "初めまして" + j.name + "です。" } func New(name string, age int, weight int) *Japanese { return &Japanese{Human{name, age}, weight} } 先ほど継承の例で挙げた例を参考にすると、 SayNameメソッドはJapanese構造体に付随されており、引数はなし、戻り値はstringとされています。 Newメソッドは引数はname、age、weightとされ、Japanese構造体のポインタを返却しています。 ※構造体とポインタはのちにて説明します 変数・定数 変数はvar、定数はconstにより定義します。 これに関してもPublic、Privateは大文字小文字で決まります。 宣言時は変数名、型の順で記述します。直接文字列、数値を入れる場合は型定義の必要がありません。 変数に値を代入したい場合は:=を利用することで宣言記述を省略できます。 定数の場合は:=を利用できません 定数で宣言できるのはstringboolcharacternumericのみです。 var name1 string var name2 = "Mike" const age1 int const age2 = 1 func main() { name3 := "Jessica" } for/if/Switch Switch/if条件/forループは()で囲む必要がなく記載ができる。{}の括りは必要。 Switch文はJavaではbreakでswitch文を抜けますがGo言語ではbreakは不要です。 Switch文はfallthroughを利用することでcase文で func main() { sum := 0 for i := 0;i < 10;i++ { sum += 1; if sum<5 { fmt.Println("合計値が5以上") } } result := judge() } func judge() string { switch sum case sum == 5: return "合計値が正しくないよ" case sum == 10: return "合計値が正しいよ" default: return "想定値以外" } struct(ストラクト):構造体 Goでは構造体にそのパッケージで利用する変数を定義します。 Javaでいうクラスに当たります。継承の記述説明を行った時同様に、処理を持たせることもできます。 type 構造体名 struct {}で宣言することができます。フィールド変数を呼び出すときは構造体名.変数名にて利用することができます。 ストラクトの初期化には「直接変数に値を入れる」「ストラクトの記載順で値を入れる」「フィールド名を指定して値を入れる」の3パターンがあります。 type Human struct { name string age int } func (h Human) SayName() string { return h.name + "です。" } func New(name string, age int) *Human { // ストラクトの順に値を入れる return &Human{name, age} } func main() { // 初期化 h := New("Mike", 20) // Humanストラクトの変数呼び出し fmt.Println(h.name) // Humanストラクトの処理呼び出し fmt.Println(h.SayAge()) // フィールド名指定で初期化する Alex := Human{name: "Alex", age: 10}; // ストラクトに直接値を入れる Alex.age = 20 } point(ポインタ):メモリアドレス ポインタはメモリのアドレス情報のことです。 GO言語では&を変数、構造体の前につけることでアドレスの取得(ポインタの取得)ができます。 また、*を変数名の前に記述することでポインタ型で変数宣言が可能です。 ポインタの存在意義が分からない場合はこちらの記事を参考にしてください Goで学ぶポインタとアドレス type Human struct { name string age int } func main() { // Human型で宣言 h1 := Human{"Mike", 20} fmt.Println(h1) // Human型の別ストラクトの宣言 h2 := h1 h2.name = "Alex" fmt.Println(h2) fmt.Println(h1) // ポインタ型で宣言 h3 := &h1 h3.name = "Alex" fmt.Println(h3) fmt.Println(h1) } 実行結果 {Mike 20} {Alex 20} {Mike 20} &{Alex 20} {Alex 20} Human型で宣言した場合はh2には代入できているが、h1に反映できていない。これを値渡しという。 ポインタ型で宣言した場合は大元のHuman型で宣言した値に対して代入されていることがわかる。これを参照渡しという。 配列 配列とは、同じ型を持つ値(要素)を並べたものです。 複数の宣言方法があります。 最初に宣言した配列のサイズを変えることはできません。 宣言方法 var 変数名 [長さ]型 var 変数名 [長さ]型 = [大きさ]型{初期値1, 初期値n} 変数名 := [...]型{初期値1, 初期値n} // 宣言方法1 var Names1 [2]string // 宣言方法2 var Names2 [2]string = [2]string{"Mike", "Alex"} // 宣言方法3 var Names3 = [...]string{"Mike", "Alex"} func main() { // 値の取得 fmt.Println(Names2[0]) fmt.Println(Names2[1]) fmt.Println(Names1, Names2, Names3) } 実行結果 Mike Alex [ ] [Mike Alex] [Mike Alex] slice(スライス):要素指定が必要ない配列 配列とは異なり長さ指定の必要がない。 別の配列から要素を取り出し参照する形での宣言やmake()を利用した宣言が可能。宣言時に値を入れることはできない。make(型,長さ,容量)で宣言が可能です。容量は省略が可能で、省略した場合長さと同じ値とされます。 配列とは異なり要素の追加が可能です(append)。 数値の意味合いは以下のようになります。 操作 意味 Slice[start:end] start から end - 1 まで Slice[start:] start から最後尾まで Slice[:end] 先頭から end - 1 まで Slice[:] 先頭から最後尾まで 長さ(length)と容量(capacity)の両方を持っています。 型が一致している場合、他のスライスに代入することが可能です。 スライスのゼロ値はnil。 宣言方法 var 変数名 []型 var 変数名 = make([]型,2,2) var arr [2]string = [2]string{"Mike", "Alex"} var slice1 []string var slice2 []string = []string{"Mike", "Alex", "jessica"} // 配列からの代入 var slice3 []string = arr[0:2] // makeを使用した宣言 // make(型,長さ,容量)スライスオブジェクトの確保、初期化を行う。容量は省略可能 var slice4 = make([]string, 2, 4) // appendによる追加 var slice5 []string = []string{"Mike", "Alex"} slice5 = append(slice5, "jessica") fmt.Println(slice1, slice2, slice3, slice4, slice5) fmt.Println(len(slice2), cap(slice2)) 実行結果 [] [Mike Alex jessica] [Mike Alex] [ ] [Mike Alex jessica] 3 3 参考記事 go言語 メリットとデメリット JavaプログラマーのためのGo言語入門 Go言語でハマったことメモ(クラス・継承) 【Go】基本文法総まとめ
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-gin + mysql + gormでAPIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。 go-ginでAPIサーバーを立ててフロント側と通信できるようにします。 OS macOS Catalina 使う技術スタック gin (ウェブサーバー) dlv (デバッグ) air (ホットリロード) gorm (ORMライブラリ) mysql golang-migrate (マイグレーション) docker あると便利なアプリ postman (rest api通信確認) goland (デバッグ、開発) sequel pro (データベース確認) ディレクトリ構造 最終的には以下のようになっています。 . ├── backend │   ├── Dockerfile.local │   ├── article │   │   └── article.go │   ├── go.mod │   ├── go.sum │   ├── handler │   │   ├── articleFunc.go │   │   └── userFunc.go │   ├── lib │   │   └── sql_handler.go │   ├── main.go │   ├── migration │   │   ├── main.go │   │   └── migrations │   │   ├── 1_articles.down.sql │   │   ├── 1_articles.up.sql │   │   ├── 2_users.down.sql │   │   └── 2_users.up.sql │   ├── tmp │   │   └── main │   └── user │   └── user.go ├── docker-compose.yml └── mysql ├── Dockerfile └── my.cnf 9 directories, 18 files 気をつけるところ github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダのところは各自書き換えてください。 ディレクトリ next-gin-mysqlという名前で立ち上げます。(nextをフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします) $ mkdir next-gin-mysql バックエンド側 まずディレクトリを作ります。 next-gin-mysql $ mkdir backend go moduleの用意 go modulesの用意をします。 github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダで初期化するとやりやすいと思います。 next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin これで ginとその依存パッケージがダウンロードされます。 それぞれのファイルの用意 サーバーを立ち上げるmain.goです。データベース用のlib、記事とユーザーのモデル、そしてハンドラはあとで書きます。 main.go package main import ( "os" "time" "github.com/gin-gonic/gin" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/greenteabiscuit/next-gin-mysql/backend/handler" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/joho/godotenv" "github.com/gin-contrib/cors" ) func main() { if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { panic(err) } } article := article.New() user := user.New() lib.DBOpen() defer lib.DBClose() r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{ "http://localhost:3000", }, AllowMethods: []string{ "POST", "GET", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", }, AllowCredentials: true, MaxAge: 24 * time.Hour, })) r.GET("/article", handler.ArticlesGet(article)) r.POST("/article", handler.ArticlePost(article)) r.POST("/user/login", handler.UserPost(user)) r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080 } 記事を扱うarticleのハンドラパッケージです。 handler/articleFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/gin-gonic/gin" ) func ArticlesGet(articles *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { result := articles.GetAll() c.JSON(http.StatusOK, result) } } type ArticlePostRequest struct { Title string `json:"title"` Description string `json:"description"` } func ArticlePost(post *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { requestBody := ArticlePostRequest{} c.Bind(&requestBody) item := article.Article{ Title: requestBody.Title, Description: requestBody.Description, } post.Add(item) c.Status(http.StatusNoContent) } } userのハンドラ関数は以下です。 handler/userFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/gin-gonic/gin" ) func UsersGet(users *user.Users) gin.HandlerFunc { return func(c *gin.Context) { result := users.GetAll() c.JSON(http.StatusOK, result) } } type UserPostRequest struct { Username string `json:"username"` Password string `json:"password"` } func UserPost(post *user.Users) gin.HandlerFunc { return func(c *gin.Context) { requestBody := UserPostRequest{} c.Bind(&requestBody) item := user.User{ Username: requestBody.Username, Password: requestBody.Password, } post.Add(item) c.Status(http.StatusNoContent) } } articleの構造体などを定義するファイルです。 article/article.go package article import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type Article struct { Title string `json:"title"` Description string `json:"description"` } type Articles struct { Items []Article } func New() *Articles { return &Articles{} } func (r *Articles) Add(a Article) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Articles) GetAll() []Article { db := lib.GetDBConn().DB var articles []Article if err := db.Find(&articles).Error; err != nil { return nil } return articles } userの構造体をまとめたファイルです。 user/user.go package user import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type User struct { Username string `json:"username"` Password string `json:"password"` } type Users struct { Items []User } func New() *Users { return &Users{} } func (r *Users) Add(a User) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Users) GetAll() []User { db := lib.GetDBConn().DB var users []User if err := db.Find(&users).Error; err != nil { return nil } return users } mysqlと接続を行う sql_handler.goも追加します。 lib/sql_handler.go package lib import ( "fmt" "os" "time" "gorm.io/gorm" "gorm.io/driver/mysql" ) // SQLHandler ... type SQLHandler struct { DB *gorm.DB Err error } var dbConn *SQLHandler // DBOpen は DB connectionを張る。 func DBOpen() { dbConn = NewSQLHandler() } // DBClose は DB connectionを張る。 func DBClose() { sqlDB, _ := dbConn.DB.DB() sqlDB.Close() } // NewSQLHandler ... func NewSQLHandler() *SQLHandler { user := os.Getenv("DB_USERNAME") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") fmt.Println(user, password, host, port) var db *gorm.DB var err error // Todo: USE_HEROKU = 1のときと場合分け if os.Getenv("USE_HEROKU") != "1" { dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo" db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } } /*else { var ( instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance' ) dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database) // dbPool is the pool of database connections. db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{}) if err != nil { panic(err) } }*/ sqlDB, _ := db.DB() //コネクションプールの最大接続数を設定。 sqlDB.SetMaxIdleConns(100) //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。 sqlDB.SetMaxOpenConns(100) //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。 sqlDB.SetConnMaxLifetime(100 * time.Second) sqlHandler := new(SQLHandler) db.Logger.LogMode(4) sqlHandler.DB = db return sqlHandler } // GetDBConn ... func GetDBConn() *SQLHandler { return dbConn } // BeginTransaction ... func BeginTransaction() *gorm.DB { dbConn.DB = dbConn.DB.Begin() return dbConn.DB } // Rollback ... func RollBack() { dbConn.DB.Rollback() } ローカル開発環境の整備 docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。 docker-compose.yml version: '3' services: go: build: context: ./backend dockerfile: Dockerfile.local volumes: - ./backend:/go/src/backend working_dir: /go/src/backend environment: TZ: Asia/Tokyo ports: - 8080:8080 - 2345:2345 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE mysql: build: ./mysql environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: backend ports: - 13306:3306 volumes: - mysql_volume:/var/lib/mysql volumes: mysql_volume: go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。 backend/Dockerfile.local FROM golang:1.15.2 COPY . /go/src/sample WORKDIR /go/src/sample RUN go get -u github.com/cosmtrek/air RUN go get -u github.com/go-delve/delve/cmd/dlv CMD ["air", "-c", ".air.toml"]** ホットリロードするために、airを使います。 airの設定ファイルである.air.tomlはairの本家レポジトリから引っ張ってくることができます。 backend/.air.toml # Config file for [Air](https://github.com/cosmtrek/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Just plain old shell command. You could use `make` as well. cmd = "go build -o ./tmp/main ." # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient" # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"] # Watch these directories if you specified. include_dir = [] # Exclude files. exclude_file = [] # Exclude unchanged files. exclude_unchanged = true # This log file places in your tmp_dir. log = "air.log" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Stop running old binary when build errors occur. stop_on_error = true # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Delay after sending Interrupt signal kill_delay = 500 # ms [log] # Show log time time = false [color] # Customize each part's color. If no color found, use the raw app log. main = "magenta" watcher = "cyan" build = "yellow" runner = "green" [misc] # Delete tmp directory on exit clean_on_exit = true mysqlの設定ファイルです。 mysql/my.cnf # MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4 mysqlのDockerfileです。 FROM mysql:8.0.21 # FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf 環境変数も設定しておきます。本番環境では見せないようにしましょう。 DB_HOSTはコンテナの名前で選ぶので、mysqlにしてあります。 backend/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend 立ち上げてみる サーバーを立ち上げてみましょう。以下のようにエラーが起きずに 2345が出てきたらとりあえずOKです。 docker-compose up --build mysql_1 | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. mysql_1 | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. go_1 | watching user go_1 | building... go_1 | running... go_1 | API server listening at: [::]:2345 golandでの設定 golandで開発するのがやりやすいと思います。 今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。 delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。 メニューのdebugをクリックし、go remoteを選びます。 適当な名前をつけて、applyとdebugをクリックします。 これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。 go_1 | API server listening at: [::]:2345 go_1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. go_1 | go_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. go_1 | - using env: export GIN_MODE=release go_1 | - using code: gin.SetMode(gin.ReleaseMode) go_1 | go_1 | [GIN-debug] GET /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers) go_1 | [GIN-debug] POST /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers) go_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default go_1 | [GIN-debug] Listening and serving HTTP on :8080 マイグレーション golang-migrateを使ってmysqlデータベースを立ち上げます。migrationというフォルダをbackend直下に作ります。 backend $ mkdir migration 以下のような構造になります。main.goでmigrationの処理を書きます。 migration $ tree . ├── main.go └── migrations ├── 1_articles.down.sql ├── 1_articles.up.sql ├── 2_users.down.sql └── 2_users.up.sql migrationの.env 先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。 migration/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend migration の main.go migration/main.go package main import ( "database/sql" "flag" "fmt" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" "github.com/joho/godotenv" "github.com/pkg/errors" ) var migrationFilePath = "file://./migrations/" func main() { fmt.Println("start migration") flag.Parse() command := flag.Arg(0) migrationFileName := flag.Arg(1) if command == "" { showUsage() os.Exit(1) } if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { fmt.Println(errors.Wrap(err, "load error .env")) } } m := newMigrate() version, dirty, _ := m.Version() force := flag.Bool("f", false, "force execute fixed sql") if dirty && *force { fmt.Println("force=true: force execute current version sql") m.Force(int(version)) } switch command { case "new": newMigration(migrationFileName) case "up": up(m) case "down": down(m) case "drop": drop(m) case "version": showVersionInfo(m.Version()) default: fmt.Println("\nerror: invalid command '", command, "'") showUsage() os.Exit(0) } } func generateDsn() string { apiRevision := os.Getenv("API_REVISION") var dsn string if apiRevision == "release" { dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応 } else { user := os.Getenv("DB_USERNAME") pass := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName) } return dsn } func newMigrate() *migrate.Migrate { dsn := generateDsn() db, openErr := sql.Open("mysql", dsn) if openErr != nil { fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()")) os.Exit(1) } driver, instanceErr := mysql.WithInstance(db, &mysql.Config{}) if instanceErr != nil { fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()")) os.Exit(1) } m, err := migrate.NewWithDatabaseInstance( migrationFilePath, "mysql", driver, ) if err != nil { fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()")) os.Exit(1) } return m } func showUsage() { fmt.Println(` ------------------------------------- Usage: go run migration/main.go <command> Commands: new FILENAME Create new up & down migration files up Apply up migrations down Apply down migrations drop Drop everything version Check current migrate version -------------------------------------`) } func newMigration(name string) { if name == "" { fmt.Println("\nerror: migration file name must be supplied as an argument") os.Exit(1) } base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name) ext := ".sql" createFile(base + ".up" + ext) createFile(base + ".down" + ext) } func createFile(fname string) { if _, err := os.Create(fname); err != nil { panic(err) } } func up(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Up() if err != nil { if err.Error() != "no change" { panic(err) } fmt.Println("\nno change") } else { fmt.Println("\nUpdated:") version, dirty, err := m.Version() showVersionInfo(version, dirty, err) } } func down(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Steps(-1) if err != nil { panic(err) } else { fmt.Println("\nUpdated:") showVersionInfo(m.Version()) } } func drop(m *migrate.Migrate) { err := m.Drop() if err != nil { panic(err) } else { fmt.Println("Dropped all migrations") return } } func showVersionInfo(version uint, dirty bool, err error) { fmt.Println("-------------------") fmt.Println("version : ", version) fmt.Println("dirty : ", dirty) fmt.Println("error : ", err) fmt.Println("-------------------") } マイグレーションのためのsqlファイル 1_articles.down.sql drop table if exists articles 1_articles.up.sql create table if not exists articles ( id integer auto_increment primary key, title varchar(40), description varchar(40) ) 2_users.down.sql drop table if exists users 2_users.up.sql create table if not exists users ( id integer auto_increment primary key, username varchar(40), password varchar(40) ) マイグレーションする dockerコンテナに入ってマイグレーションします。go run main.go upでマイグレーションが行われます。 $ docker exec -it container_name bash # cd migration # go run main.go up start migration Before: ------------------- version : 0 dirty : false error : no migration ------------------- Updated: ------------------- version : 2 dirty : false error : <nil> ------------------- これでdbでもテーブルができているのを確認したらOKです! Sequel Proなどのアプリで確認できます。 PostmanでのAPI確認 これは Postmanというデスクトップアプリで確認することができます。 Post JSON形式で送ります。 GET データベースにも hoge と hello worldが入っているのが確認できました。 まとめ air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。 今後試してみたいこと herokuにデプロイしてみる フロントエンド(next + reactなど)とつなげてみる(そもそもレポジトリ名にnextを含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。) 参考 go-ginでサクッとRESTAPIを構築する
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

go-gin + mysql + gorm + Dockerで開発用APIサーバーを立ててみた

簡易なウェブアプリが作りたくて、そのときのメモです。 go-ginでAPIサーバーを立ててデータベースと通信できるようにします。 Github Repositoryを作りました。 OS macOS Catalina 使う技術スタック gin (ウェブサーバー) dlv (デバッグ) air (ホットリロード) gorm (ORMライブラリ) mysql golang-migrate (マイグレーション) docker あると便利なアプリ postman (rest api通信確認) goland (デバッグ、開発) sequel pro (データベース確認) ディレクトリ構造 最終的には以下のようになっています。 . ├── backend │   ├── Dockerfile.local │   ├── article │   │   └── article.go │   ├── go.mod │   ├── go.sum │   ├── handler │   │   ├── articleFunc.go │   │   └── userFunc.go │   ├── lib │   │   └── sql_handler.go │   ├── main.go │   ├── migration │   │   ├── main.go │   │   └── migrations │   │   ├── 1_articles.down.sql │   │   ├── 1_articles.up.sql │   │   ├── 2_users.down.sql │   │   └── 2_users.up.sql │   ├── tmp │   │   └── main │   └── user │   └── user.go ├── docker-compose.yml └── mysql ├── Dockerfile └── my.cnf 9 directories, 18 files 気をつけるところ github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダのところは各自書き換えてください。 ディレクトリ next-gin-mysqlという名前で立ち上げます。(nextをフロントエンドとして立ち上げたいのですが、記事の長さ的にそれは次回にします) $ mkdir next-gin-mysql バックエンド側 まずディレクトリを作ります。 next-gin-mysql $ mkdir backend go moduleの用意 go modulesの用意をします。 github.com/自分のアカウント/自分のレポジトリ/main.goが存在するフォルダで初期化するとやりやすいと思います。 next-gin-mysql/backend $ go mod init github.com/greenteabiscuit/next-gin-mysql/backend next-gin-mysql/backend $ go get -u github.com/gin-gonic/gin これで ginとその依存パッケージがダウンロードされます。 それぞれのファイルの用意 サーバーを立ち上げるmain.goです。データベース用のlib、記事とユーザーのモデル、そしてハンドラはあとで書きます。 main.go package main import ( "os" "time" "github.com/gin-gonic/gin" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/greenteabiscuit/next-gin-mysql/backend/handler" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/joho/godotenv" "github.com/gin-contrib/cors" ) func main() { if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { panic(err) } } article := article.New() user := user.New() lib.DBOpen() defer lib.DBClose() r := gin.Default() r.Use(cors.New(cors.Config{ AllowOrigins: []string{ "http://localhost:3000", }, AllowMethods: []string{ "POST", "GET", "OPTIONS", }, AllowHeaders: []string{ "Access-Control-Allow-Credentials", "Access-Control-Allow-Headers", "Content-Type", "Content-Length", "Accept-Encoding", "Authorization", }, AllowCredentials: true, MaxAge: 24 * time.Hour, })) r.GET("/article", handler.ArticlesGet(article)) r.POST("/article", handler.ArticlePost(article)) r.POST("/user/login", handler.UserPost(user)) r.Run(os.Getenv("HTTP_HOST") + ":" + os.Getenv("HTTP_PORT")) // listen and serve on 0.0.0.0:8080 } 記事を扱うarticleのハンドラパッケージです。 handler/articleFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/article" "github.com/gin-gonic/gin" ) func ArticlesGet(articles *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { result := articles.GetAll() c.JSON(http.StatusOK, result) } } type ArticlePostRequest struct { Title string `json:"title"` Description string `json:"description"` } func ArticlePost(post *article.Articles) gin.HandlerFunc { return func(c *gin.Context) { requestBody := ArticlePostRequest{} c.Bind(&requestBody) item := article.Article{ Title: requestBody.Title, Description: requestBody.Description, } post.Add(item) c.Status(http.StatusNoContent) } } userのハンドラ関数は以下です。 handler/userFunc.go package handler import ( "net/http" "github.com/greenteabiscuit/next-gin-mysql/backend/user" "github.com/gin-gonic/gin" ) func UsersGet(users *user.Users) gin.HandlerFunc { return func(c *gin.Context) { result := users.GetAll() c.JSON(http.StatusOK, result) } } type UserPostRequest struct { Username string `json:"username"` Password string `json:"password"` } func UserPost(post *user.Users) gin.HandlerFunc { return func(c *gin.Context) { requestBody := UserPostRequest{} c.Bind(&requestBody) item := user.User{ Username: requestBody.Username, Password: requestBody.Password, } post.Add(item) c.Status(http.StatusNoContent) } } articleの構造体などを定義するファイルです。 article/article.go package article import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type Article struct { Title string `json:"title"` Description string `json:"description"` } type Articles struct { Items []Article } func New() *Articles { return &Articles{} } func (r *Articles) Add(a Article) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Articles) GetAll() []Article { db := lib.GetDBConn().DB var articles []Article if err := db.Find(&articles).Error; err != nil { return nil } return articles } userの構造体をまとめたファイルです。ちょっとここはテキトーなのでもう少し直したい、、、 user/user.go package user import ( "fmt" "github.com/greenteabiscuit/next-gin-mysql/backend/lib" ) type User struct { Username string `json:"username"` Password string `json:"password"` } type Users struct { Items []User } func New() *Users { return &Users{} } func (r *Users) Add(a User) { r.Items = append(r.Items, a) db := lib.GetDBConn().DB if err := db.Create(a).Error; err != nil { fmt.Println("err!") } } func (r *Users) GetAll() []User { db := lib.GetDBConn().DB var users []User if err := db.Find(&users).Error; err != nil { return nil } return users } mysqlと接続を行う sql_handler.goも追加します。 lib/sql_handler.go package lib import ( "fmt" "os" "time" "gorm.io/gorm" "gorm.io/driver/mysql" ) // SQLHandler ... type SQLHandler struct { DB *gorm.DB Err error } var dbConn *SQLHandler // DBOpen は DB connectionを張る。 func DBOpen() { dbConn = NewSQLHandler() } // DBClose は DB connectionを張る。 func DBClose() { sqlDB, _ := dbConn.DB.DB() sqlDB.Close() } // NewSQLHandler ... func NewSQLHandler() *SQLHandler { user := os.Getenv("DB_USERNAME") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") fmt.Println(user, password, host, port) var db *gorm.DB var err error // Todo: USE_HEROKU = 1のときと場合分け if os.Getenv("USE_HEROKU") != "1" { dsn := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + dbName + "?parseTime=true&loc=Asia%2FTokyo" db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) if err != nil { panic(err) } } /*else { var ( instanceConnectionName = os.Getenv("DB_CONNECTION_NAME") // e.g. 'project:region:instance' ) dbURI := fmt.Sprintf("%s:%s@unix(/cloudsql/%s)/%s?parseTime=true", user, password, instanceConnectionName, database) // dbPool is the pool of database connections. db, err = gorm.Open(mysql.Open(dbURI), &gorm.Config{}) if err != nil { panic(err) } }*/ sqlDB, _ := db.DB() //コネクションプールの最大接続数を設定。 sqlDB.SetMaxIdleConns(100) //接続の最大数を設定。 nに0以下の値を設定で、接続数は無制限。 sqlDB.SetMaxOpenConns(100) //接続の再利用が可能な時間を設定。dに0以下の値を設定で、ずっと再利用可能。 sqlDB.SetConnMaxLifetime(100 * time.Second) sqlHandler := new(SQLHandler) db.Logger.LogMode(4) sqlHandler.DB = db return sqlHandler } // GetDBConn ... func GetDBConn() *SQLHandler { return dbConn } // BeginTransaction ... func BeginTransaction() *gorm.DB { dbConn.DB = dbConn.DB.Begin() return dbConn.DB } // Rollback ... func RollBack() { dbConn.DB.Rollback() } ローカル開発環境の整備 docker-composeで一発でサーバーをすべて立ち上げるようにします。このときについでに mysqlの準備とデバッグサーバーの準備もしておきます。 docker-compose.yml version: '3' services: go: build: context: ./backend dockerfile: Dockerfile.local volumes: - ./backend:/go/src/backend working_dir: /go/src/backend environment: TZ: Asia/Tokyo ports: - 8080:8080 - 2345:2345 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE mysql: build: ./mysql environment: TZ: Asia/Tokyo MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: backend ports: - 13306:3306 volumes: - mysql_volume:/var/lib/mysql volumes: mysql_volume: go用のDockerfileです。今後デプロイ用のDockerfileと分けるかもしれないので、一応Dockerfile.localにしておきます。 backend/Dockerfile.local FROM golang:1.15.2 COPY . /go/src/sample WORKDIR /go/src/sample RUN go get -u github.com/cosmtrek/air RUN go get -u github.com/go-delve/delve/cmd/dlv CMD ["air", "-c", ".air.toml"]** ホットリロードするために、airを使います。 airの設定ファイルである.air.tomlはairの本家レポジトリから引っ張ってくることができます。 backend/.air.toml # Config file for [Air](https://github.com/cosmtrek/air) in TOML format # Working directory # . or absolute path, please note that the directories following must be under root. root = "." tmp_dir = "tmp" [build] # Just plain old shell command. You could use `make` as well. cmd = "go build -o ./tmp/main ." # Binary file yields from `cmd`. bin = "tmp/main" # Customize binary. full_bin = "APP_ENV=dev APP_USER=air /go/bin/dlv exec ./tmp/main --headless=true --listen=:2345 --api-version=2 --accept-multiclient" # Watch these filename extensions. include_ext = ["go", "tpl", "tmpl", "html"] # Ignore these filename extensions or directories. exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules", "storage"] # Watch these directories if you specified. include_dir = [] # Exclude files. exclude_file = [] # Exclude unchanged files. exclude_unchanged = true # This log file places in your tmp_dir. log = "air.log" # It's not necessary to trigger build each time file changes if it's too frequent. delay = 1000 # ms # Stop running old binary when build errors occur. stop_on_error = true # Send Interrupt signal before killing process (windows does not support this feature) send_interrupt = false # Delay after sending Interrupt signal kill_delay = 500 # ms [log] # Show log time time = false [color] # Customize each part's color. If no color found, use the raw app log. main = "magenta" watcher = "cyan" build = "yellow" runner = "green" [misc] # Delete tmp directory on exit clean_on_exit = true mysqlの設定ファイルです。 mysql/my.cnf # MySQLサーバーへの設定 [mysqld] # 文字コード/照合順序の設定 character_set_server=utf8mb4 collation_server=utf8mb4_bin # タイムゾーンの設定 default_time_zone=SYSTEM log_timestamps=SYSTEM # デフォルト認証プラグインの設定 default_authentication_plugin=mysql_native_password # mysqlオプションの設定 [mysql] # 文字コードの設定 default_character_set=utf8mb4 # mysqlクライアントツールの設定 [client] # 文字コードの設定 default_character_set=utf8mb4 mysqlのDockerfileです。 FROM mysql:8.0.21 # FROM mysql@sha256:77b7e09c906615c1bb59b2e9d7703f728b1186a5a70e547ce2f1079ef4c1c5ca RUN echo "USE mysql;" > /docker-entrypoint-initdb.d/timezones.sql && mysql_tzinfo_to_sql /usr/share/zoneinfo >> /docker-entrypoint-initdb.d/timezones.sql COPY ./my.cnf /etc/mysql/conf.d/my.cnf 環境変数も設定しておきます。本番環境では見せないようにしましょう。 DB_HOSTはコンテナの名前で選ぶので、mysqlにしてあります。 backend/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend 立ち上げてみる サーバーを立ち上げてみましょう。以下のようにエラーが起きずに 2345が出てきたらとりあえずOKです。 docker-compose up --build mysql_1 | 2021-05-04T16:29:33.735276+09:00 0 [Warning] [MY-011810] [Server] Insecure configuration for --pid-file: Location '/var/run/mysqld' in the path is accessible to all OS users. Consider choosing a different directory. mysql_1 | 2021-05-04T16:29:33.770064+09:00 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL. go_1 | watching user go_1 | building... go_1 | running... go_1 | API server listening at: [::]:2345 golandでの設定 golandで開発するのがやりやすいと思います。 今はデバッグのサーバしか立ち上がっていないので、ここで設定を行います。 delveのプロセスを走らせることで、APIサーバーも立ち上がるようになります。 メニューのdebugをクリックし、go remoteを選びます。 適当な名前をつけて、applyとdebugをクリックします。 これでコマンドラインでもサーバが立ち上がっています。ただまだマイグレーションができていないので、データベースなどからデータが取得できません。 go_1 | API server listening at: [::]:2345 go_1 | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. go_1 | go_1 | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. go_1 | - using env: export GIN_MODE=release go_1 | - using code: gin.SetMode(gin.ReleaseMode) go_1 | go_1 | [GIN-debug] GET /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlesGet.func1 (3 handlers) go_1 | [GIN-debug] POST /article --> github.com/greenteabiscuit/next-gin-mysql/backend/handler.ArticlePost.func1 (3 handlers) go_1 | [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default go_1 | [GIN-debug] Listening and serving HTTP on :8080 マイグレーション golang-migrateを使ってmysqlデータベースを立ち上げます。migrationというフォルダをbackend直下に作ります。 backend $ mkdir migration 以下のような構造になります。main.goでmigrationの処理を書きます。 migration $ tree . ├── main.go └── migrations ├── 1_articles.down.sql ├── 1_articles.up.sql ├── 2_users.down.sql └── 2_users.up.sql migrationの.env 先ほどと同じになりますが、なるべく見せないようにしましょう。今回はローカルなので大丈夫かと思いますが。 migration/.env HTTP_HOST="" HTTP_PORT=8080 DB_HOST="mysql" DB_PORT=3306 DB_USERNAME=root DB_PASSWORD=root DB_DATABASE=backend migration の main.go migration/main.go package main import ( "database/sql" "flag" "fmt" "os" "time" _ "github.com/go-sql-driver/mysql" "github.com/golang-migrate/migrate" "github.com/golang-migrate/migrate/database/mysql" _ "github.com/golang-migrate/migrate/source/file" "github.com/joho/godotenv" "github.com/pkg/errors" ) var migrationFilePath = "file://./migrations/" func main() { fmt.Println("start migration") flag.Parse() command := flag.Arg(0) migrationFileName := flag.Arg(1) if command == "" { showUsage() os.Exit(1) } if os.Getenv("USE_HEROKU") != "1" { err := godotenv.Load() if err != nil { fmt.Println(errors.Wrap(err, "load error .env")) } } m := newMigrate() version, dirty, _ := m.Version() force := flag.Bool("f", false, "force execute fixed sql") if dirty && *force { fmt.Println("force=true: force execute current version sql") m.Force(int(version)) } switch command { case "new": newMigration(migrationFileName) case "up": up(m) case "down": down(m) case "drop": drop(m) case "version": showVersionInfo(m.Version()) default: fmt.Println("\nerror: invalid command '", command, "'") showUsage() os.Exit(0) } } func generateDsn() string { apiRevision := os.Getenv("API_REVISION") var dsn string if apiRevision == "release" { dsn = os.Getenv("DATABASE_URL") + "&multiStatements=true" // heroku対応 } else { user := os.Getenv("DB_USERNAME") pass := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") dbName := os.Getenv("DB_DATABASE") dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&multiStatements=true", user, pass, host, port, dbName) } return dsn } func newMigrate() *migrate.Migrate { dsn := generateDsn() db, openErr := sql.Open("mysql", dsn) if openErr != nil { fmt.Println(errors.Wrap(openErr, "error occurred. sql.Open()")) os.Exit(1) } driver, instanceErr := mysql.WithInstance(db, &mysql.Config{}) if instanceErr != nil { fmt.Println(errors.Wrap(instanceErr, "error occurred. mysql.WithInstance()")) os.Exit(1) } m, err := migrate.NewWithDatabaseInstance( migrationFilePath, "mysql", driver, ) if err != nil { fmt.Println(errors.Wrap(err, "error occurred. migrate.NewWithDatabaseInstance()")) os.Exit(1) } return m } func showUsage() { fmt.Println(` ------------------------------------- Usage: go run migration/main.go <command> Commands: new FILENAME Create new up & down migration files up Apply up migrations down Apply down migrations drop Drop everything version Check current migrate version -------------------------------------`) } func newMigration(name string) { if name == "" { fmt.Println("\nerror: migration file name must be supplied as an argument") os.Exit(1) } base := fmt.Sprintf("./migration/migrations/%s_%s", time.Now().Format("20060102030405"), name) ext := ".sql" createFile(base + ".up" + ext) createFile(base + ".down" + ext) } func createFile(fname string) { if _, err := os.Create(fname); err != nil { panic(err) } } func up(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Up() if err != nil { if err.Error() != "no change" { panic(err) } fmt.Println("\nno change") } else { fmt.Println("\nUpdated:") version, dirty, err := m.Version() showVersionInfo(version, dirty, err) } } func down(m *migrate.Migrate) { fmt.Println("Before:") showVersionInfo(m.Version()) err := m.Steps(-1) if err != nil { panic(err) } else { fmt.Println("\nUpdated:") showVersionInfo(m.Version()) } } func drop(m *migrate.Migrate) { err := m.Drop() if err != nil { panic(err) } else { fmt.Println("Dropped all migrations") return } } func showVersionInfo(version uint, dirty bool, err error) { fmt.Println("-------------------") fmt.Println("version : ", version) fmt.Println("dirty : ", dirty) fmt.Println("error : ", err) fmt.Println("-------------------") } マイグレーションのためのsqlファイル 1_articles.down.sql drop table if exists articles 1_articles.up.sql create table if not exists articles ( id integer auto_increment primary key, title varchar(40), description varchar(40) ) 2_users.down.sql drop table if exists users 2_users.up.sql create table if not exists users ( id integer auto_increment primary key, username varchar(40), password varchar(40) ) マイグレーションする dockerコンテナに入ってマイグレーションします。go run main.go upでマイグレーションが行われます。 $ docker exec -it container_name bash # cd migration # go run main.go up start migration Before: ------------------- version : 0 dirty : false error : no migration ------------------- Updated: ------------------- version : 2 dirty : false error : <nil> ------------------- これでdbでもテーブルができているのを確認したらOKです! Sequel Proなどのアプリで確認できます。 PostmanでのAPI確認 これは Postmanというデスクトップアプリで確認することができます。 Post JSON形式で送ります。 GET データベースにも hoge と hello worldが入っているのが確認できました。 コマンドラインでもカラフルに表示されています。 まとめ air + gin + gorm + golang-migrate + mysql + delve + dockerで開発用APIサーバーを立ち上げることができました。 今後試してみたいこと herokuにデプロイしてみる userのところの処理がテキトーなままなので直します、、、 フロントエンド(next + reactなど)とつなげてみる(そもそもレポジトリ名にnextを含んでいたので繋げようと思ったのですが、記事が長すぎてしまったので次回以降やりたいと思います。) 参考 go-ginでサクッとRESTAPIを構築する golang,docker,mysqlの環境をherokuにデプロイする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

『ホットリロード x Go言語 x MySQL』なHTTPサーバのDocker環境構築手順

こちらに移動しました。 https://nishinatoshiharu.com/install-go-air/
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でデータベース(MySQL)に接続する方法

Go言語でデータベースに接続するための方法について紹介します。 今回はMySQLを利用します。 Go言語でデータベースへ接続する手順 データベース接続のパッケージをインポート 接続するデータベースのドライバをインポート データベースのオープン 以下では各手順について紹介します。 データベース接続のパッケージをインポート データベースに接続するためのパッケージをインポートします。 Go言語の標準パッケージではdatabase/sqlが該当します。 import "database/sql" 接続するデータベースのドライバをインポート データベースに接続するにはドライバが必要です。ドライバはデータベースの種類ごとに用意されています。 たとえばMySQLの場合はgithub.com/go-sql-driver/mysqlが該当します。 ドライバのパッケージは直接コード上で利用しないのでアンダースコア(_)でインポートします。(blank identifier) import _ "github.com/go-sql-driver/mysql" 各種データベースのドライバはこちらで確認ができます。 データベースのオープン データベースへアクセスするためのオブジェクトを作成します。 database/sqlの場合、Open関数を利用してdb, err := sql.Open("ドライバ名", "接続文字列")のようにすると変数dbを通じてデータベースへアクセスできます。 "接続文字列"は各種ドライバのドキュメントを参考に記載します。MySQLの場合はこちらににフォーマットが記載されています。 動作確認 実際にGo言語からデータベースに接続してみます。 下準備: 検証用のデータベースをDockerで用意する 今回はMySQLのDockerイメージを利用して検証用のデータベースを構築します。 データベース接続情報 ユーザー名: webuser パスワード: webpass データベース名: go_mysql8_development ポート: 3306 docker-compose.yml version: '3' services: db: image: mysql:8.0.21 ports: - '3306:3306' command: --default-authentication-plugin=mysql_native_password environment: MYSQL_USER: 'webuser' MYSQL_PASSWORD: 'webpass' MYSQL_ROOT_PASSWORD: 'pass' MYSQL_DATABASE: 'go_mysql8_development' docker-compose.ymlでコンテナ起動後、以下のようにデータベースに接続できればOKです。 ### コンテナの起動 $ cd /path/to/docker-compose-file $ docker-compose up -d ### dbコンテナにアクセスし、データベースがあることを確認 $ docker-compose exec db mysql -uwebuser -pwebpass -D go_mysql8_development -e "show databases" +-----------------------+ | Database | +-----------------------+ | go_mysql8_development | | information_schema | +-----------------------+ サンプルコードの実装と実行 最終的なサンプルコードは以下のようになります。 sql.Openはあくまでデータベースへ接続するオブジェクトを生成しているだけです。 ですので、実際のデータベースへの接続テストはdb.Ping()で確認します。 main.go package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func main() { // [ユーザ名]:[パスワード]@tcp([ホスト名]:[ポート番号])/[データベース名]?charset=[文字コード] dbconf := "webuser:webpass@tcp(127.0.0.1:3306)/go_mysql8_development?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) // 接続が終了したらクローズする defer db.Close() if err != nil { fmt.Println(err.Error()) } err = db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } サンプルコードを実行し、以下のように『データベース接続成功』の文字列が表示されればOKです。 ### 作業ディレクトリへ移動 $ cd /path/to/project ### パッケージのインストール $ go mod init example $ go mod tidy ### 実行 $ go run main.go データベース接続成功 参考: データベース接続のソースコードをリファクタリングする 上記で紹介したソースコードは以下の改善点があります。 データベース接続のロジックがmain関数に直接書かれている データベース情報がソースコードにベタ書きされている ここからはソースコードのリファクタリング方法について紹介します。 データベース接続のロジックを別パッケージ化する データベース接続のロジックを別パッケージにすることでmain関数をシンプルにします。 main.go package main import ( + "example/database" // "モジュール名/パッケージ名" "fmt" - "database/sql" - _ "github.com/go-sql-driver/mysql" ) func main() { + db := database.Connect() - dbconf := "webuser:webpass@tcp(127.0.0.1:3306)/go_mysql8_development?charset=utf8mb4" - - db, err := sql.Open("mysql", dbconf) - if err != nil { - fmt.Println(err.Error()) - } defer db.Close() err := db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } データベース接続に関するパッケージは以下の通りです。 外部パッケージから呼び出せるようにするため、関数名は大文字から始めます。 database/connect.go package database import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { user := "webuser" password := "webpass" host := "localhost" port := "3306" database_name := "go_mysql8_development" dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } GoDotEnvを導入してデータベース接続情報を.envファイルに配置する GoDotEnvはGo言語で.envファイルを利用できるようにするパッケージです。 GoDotEnvを利用することでデータベース接続情報を環境変数に置き換えられます。 GoDotEnvはgodotenv.Load()で環境変数をロードし、os.Getenv("Key名")で値を呼び出します。 GoDotEnvを利用したdatabaseパッケージのソースコードは以下の通りです。 .env DB_USER=webuser DB_PASSWORD=webpass DB_HOST=db DB_PORT=3306 DB_DATABASE_NAME=go_mysql8_development database/connect.go package database import ( "database/sql" "fmt" "os" "github.com/joho/godotenv" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { err := godotenv.Load() if err != nil { fmt.Println(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } 最終的なソースコード リファクタリングを終えた最終的なソースコードは以下の通りです。 main.go package main import ( "example/database" "fmt" ) func main() { db := database.Connect() defer db.Close() err := db.Ping() if err != nil { fmt.Println("データベース接続失敗") return } else { fmt.Println("データベース接続成功") } } database/connect.go package database import ( "database/sql" "fmt" "os" "github.com/joho/godotenv" _ "github.com/go-sql-driver/mysql" ) func Connect() *sql.DB { err := godotenv.Load() if err != nil { fmt.Println(err.Error()) } user := os.Getenv("DB_USER") password := os.Getenv("DB_PASSWORD") host := os.Getenv("DB_HOST") port := os.Getenv("DB_PORT") database_name := os.Getenv("DB_DATABASE_NAME") dbconf := user + ":" + password + "@tcp(" + host + ":" + port + ")/" + database_name + "?charset=utf8mb4" db, err := sql.Open("mysql", dbconf) if err != nil { fmt.Println(err.Error()) } return db } .env DB_USER=webuser DB_PASSWORD=webpass DB_HOST=db DB_PORT=3306 DB_DATABASE_NAME=go_mysql8_development まとめ Go言語でデータベースに接続する方法 データベース接続のパッケージをインポート 接続するデータベースのドライバをインポート データベース接続情報をドライバのパラメータに渡す データベースをオープンし、オブジェクトを生成 オブジェクトを通じてデータベースを操作する 認識違いや補足があればコメントいただけると助かります。 参考 プログラミング言語Go完全入門: 11. データベース 【Go実践】GoでMySQLを使おう(1) – ドライバのインストールからデータベース接続まで Go database/sql の操作ガイドあったんかい
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む