20220108のGoに関する記事は8件です。

GoでのWeb Framework選択時にルーティングの問題でGinを諦めてしまった人へ

前置き 久しぶりに記事を書いています。(結構雑にですが) なんで書く気になったかというと、あまりに嬉しい修正が入っていたことに気が付いたからです。 恥ずかしながら結構前のアップデートだったようですが、12月中頃に気付きました・・・ GoでAPIサーバを作成する際にWAFの選択肢は様々だと思います。 go-swaggerを使ってみようかなんて考えたり、EchoとGinってどっちが良いんだろうと悩んだり。 ちなみに僕は過去にGinを選択したり、WAFというかRouterとしてgorilla/muxを使用したりもしていました。 そしてGinについてはRouterのPathの記述というか、ルーティングに悩まされることがありました。 いくつかそのような記事も見かけたので気になる方は検索してみてください。 上記のようにPathが柔軟に作れないことで一定数Ginを選択肢から外してしまった方もいるのでは無いでしょうか。 今回はそんなルーティングに難があったGinでかなり問題が解決されていた部分について書いていこうと思う。 前提 go version: go1.17.6 darwin/arm64 問題があったGinのversion: ~ https://github.com/gin-gonic/gin@v1.6.3 問題が解消されたGinのversion: https://github.com/gin-gonic/gin@v1.7.0 ~ 今までの問題 ~v1.6.3 APIサーバなどを作成する際のルーティングで衝突してしまう問題 下記のようなルーティングを記載したとします ※GinではPathParametersを使用する際には:PathParametersとして記述する package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/:path", func(c *gin.Context) { path := c.Param("path") c.JSON(200, gin.H{"message": path}) }) r.Run() } このようなルーティングでは正しく機能します。 $ curl localhost:8080/ok {"message":"ok"} $ curl localhost:8080/sample {"message":"sample"} $ curl localhost:8080/test {"message":"test"} しかし上記のルーティングのように一番上の階層でPathParametersを使用してしまった場合 下記のようなルーティングを記述すると衝突して起動できなくなってしまっていました。 package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/:path", func(c *gin.Context) { path := c.Param("path") c.JSON(200, gin.H{"message": path}) }) //追加した部分Start r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) //追加した部分End r.Run() } $ go run main.go panic: 'ping' in new path '/ping' conflicts with existing wildcard ':path' in existing prefix '/:path' /:PathParameters /ping 第一階層を/pingとPathParametersで受け付けたそれ以外にしたかったのですが衝突してしまい起動すらできません。 この問題は第一階層だけに限った話ではなく同一階層でPathParametersと記述したPathを区別できず衝突してしまうというものでした。(というか解決される順番の問題) できない例 /users/:id /users/search できる例 下記のようにuserとusersを分ける必要がある。 /user/:id /users/search 上記の例は少し乱暴というか分ける設計で行くのが自然に見えるので分かり辛いかもしれませんが ある程度制約が生まれてしまっていることが分かっていただければと思います。 他にも/が入ったPathParametersの/を自動的に削除してしまうという問題があったが殆どの原因が上記のPathParametersが記述したPathと衝突しまう問題と絡んでいたものなので割愛。 問題が解消された v1.7.0 ~ 先ほど起動できなかったもの package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/:path", func(c *gin.Context) { path := c.Param("path") c.JSON(200, gin.H{"message": path}) }) //追加した部分Start r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong"}) }) //追加した部分End r.Run() } 問題なく起動できる $ go run ./main.go [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /:path --> main.main.func1 (3 handlers) [GIN-debug] GET /ping --> main.main.func2 (3 handlers) [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 ルーティングも問題ない $ curl localhost:8080/test {"message":"test"} $ curl localhost:8080/sample {"message":"sample"} $ curl localhost:8080/ping {"message":"pong"} 以上!!! 今までルーティングの問題でGinを諦めてしまった人はもう一度選択肢の中に含めても良いかもしれません。 誰かの技術選択の際に少しでも参考になれば嬉しいです。 ちなみにどんな変更だったかは検索ツリーの変更を行いワイルドカード(PathParameters)が解決される順番を前にしたとのことです。 詳しい内容は対象のPRを見てみるのも良いかと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[golang error] go: gopkg.in/yaml.v2@v2.2.2: unknown revision v2.2.2

状況 Dockerでgolangとmysqlの環境構築 エラー内容 go: gopkg.in/yaml.v2@v2.2.2: unknown revision v2.2.2 go: error loading module requirements さきに結論 version (1.17)にupdateしましょう 上記をすでに試した場合 より詳しく 結論の繰り返しになりますが versionを更新することで解決できます 参考にしたissueはこちら 解決できてない方は自分と同じ場合かもしれないので 先にお進みください 自分の場合 Dockerfile FROM golang:1.17 WORKDIR /go/src/github.com/mahiro72/todo_api_pra COPY . . ENV GO111MODULE=on そもそもversionが本当に更新されているか コンテナ内で確認してみてください コンテナが動いてる場合 ログインしてgo versionで調べられます コンテナがbuild時に失敗し強制終了している場合 Dockerflieまたはcomposeのcmd で go versionを調べてみてください example: Dokcerfileの場合 CMD ["go","version"] 解決 自分の場合はレアケース(?)でversion指定してても buildするとversion:1.11.4になってました 上のDockerfileにあるENV GO111MODULE=onを コメント化することでversionを1.17にできました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[golang error] go: gopkg.in/yaml.v2@v2.2.2: unknown revision v2.2.2を解決した話

状況 Dockerでgolangとmysqlの環境構築 エラー内容 go: gopkg.in/yaml.v2@v2.2.2: unknown revision v2.2.2 go: error loading module requirements さきに結論 version (1.17)にupdateしましょう 上記をすでに試した場合 より詳しく 結論の繰り返しになりますが versionを更新することで解決できます 参考にしたissueはこちら 解決できてない方は自分と同じ場合かもしれないので 先にお進みください そもそもversionが本当に更新されているか コンテナ内で確認してみてください コンテナが動いてる場合 ログインしてgo versionで調べられます コンテナがbuild時に失敗し強制終了している場合 Dockerflieまたはcomposeのcmd で go versionを調べてみてください 例: Dokcerfileの場合 CMD ["go","version"] 解決 自分の場合はversion:1.11.4になってました
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

multipass + docker on Mac

Mac でのDocker環境構築 いよいよ DockerDesktopの猶予期限がくるので、移行しました。 下記どちらでコマンドをうっているのか分かりづらいので セクションタイトルに Macで実行する場合は "on Mac"、 仮想環境のUbuntuで実行する場合は "on Ubuntu"と入れる multipass (ubuntu の仮想起動環境)インストール on Mac https://multipass.run/ 1. 上記からmac用binaryをDownload 2. installer を起動 multipass でubuntu:latest(20.04) 起動 on Mac あとからmemory, disk sizeを変更するのがとても大変なので最初から必要な分指定する。目的としてkubernetesのローカルデバッグをkindを使用して動かすので以下のようにmemory 10G, disk 20G, cpu core 4 程度は最低必要。 % multipass launch --name ryujidev -m 10G -d 20G -c 4 Launched: ryujidev % multipass ls Name State IPv4 Image ryujidev Running 192.168.64.3 Ubuntu 20.04 LTS % multipass info ryujidev Name: ryujidev State: Running IPv4: 192.168.64.3 172.17.0.1 172.18.0.1 Release: Ubuntu 20.04.3 LTS Image hash: 8fbc4e8c6e33 (Ubuntu 20.04 LTS) Load: 0.98 0.89 1.09 Disk usage: 12.3G out of 19.2G Memory usage: 2.3G out of 9.7G Mounts: -- あとから cpu, memoryを変更する方法 最初に指定していたほうがよいのですが、kubernetesでpodを起動しようとして、 podのstatusがpendingのままで進まないということがよくあります。 kubectrl describe pods をするとCPUやメモリが足りていないというのがわかります。 $kubectl describe pods -n istio-system ... Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedScheduling 27m (x1 over 28m) default-scheduler 0/1 nodes are available: 1 Insufficient cpu. Warning FailedScheduling 15m (x1 over 16m) default-scheduler 0/1 nodes are available: 1 Insufficient cpu. Warning FailedScheduling 14h (x365 over 20h) default-scheduler 0/1 nodes are available: 1 Insufficient cpu. こんなときはしょうがないので以下の手順でmultipassが割り当てる cpu, memoryを変更できます https://github.com/canonical/multipass/issues/1158#issuecomment-548073024 # stop multipassd sudo launchctl unload /Library/LaunchDaemons/com.canonical.multipassd.plist # edit /var/root/Library/Application Support/multipassd/multipassd-vm-instances.json # you'll need sudo for that # start multipassd again sudo launchctl load /Library/LaunchDaemons/com.canonical.multipassd.plist 以下が編集したあとのmultipassd-vm-instances.jsonの例です { "ryujidev": { "deleted": false, "disk_space": "21474836480", "extra_interfaces": [ ], "mac_addr": "52:54:00:e1:9d:18", "mem_size": "10737418240", "metadata": { }, "mounts": [ ], "num_cores": 4, "ssh_username": "ubuntu", "state": 4 } } 参考:あとからDiskSizeを変更する方法 下記に説明があるので、やってみたのですが 2回ともmultipassでstartし直そうとすると disk imageが壊れてしまったようでした。 深くおっていないですがdisk sizeは変えなくて良いように最初に多めに割り当てておくのが無難です。 ubuntu に local directory をmount, login on Mac mountするかどうかは任意 (base) ~ % multipass mount ~/git ryujidev:/home/ubuntu/git (base) ~ % multipass shell ryujidev Welcome to Ubuntu 20.04.3 LTS (GNU/Linux 5.4.0-92-generic x86_64) * Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage System information as of Sat Jan 8 09:05:44 JST 2022 System load: 0.13 Usage of /: 28.0% of 4.67GB Memory usage: 2% Swap usage: 0% Processes: 119 Users logged in: 0 IPv4 address for enp0s2: 192.168.64.3 IPv6 address for enp0s2: fd76:3e59:29b1:1bdf:4449:33ff:fe91:8c20 1 update can be applied immediately. To see these additional updates run: apt list --upgradable Last login: Sat Jan 8 09:04:25 2022 from 192.168.64.1 ubuntu@ryujidev:~$ ls git snap docker Install on Ubuntu https://docs.docker.com/engine/install/ubuntu/ 上記を直接参照したほうがよいですが何度も再実行するときのメモ用に下記にコマンドを抜き出しておきます。 sudo apt-get remove docker docker-engine docker.io ubuntu@ryujidev:~/git$ sudo apt-get update sudo apt-get install ca-certificates curl gnupg lsb-release curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install docker-ce docker-ce-cli containerd.io sudo docker run hello-world sudo なしでdocker実行するためのGroup追加 on Ubuntu sudo gpasswd -a $USER docker # logout & login, then enabled. Go package and make install on Ubuntu sudo add-apt-repository ppa:longsleep/golang-backports sudo apt update sudo apt install golang-go sudo apt install make remote debbug on Mac まず下記のように multipass 内で生成されたssh 公開鍵をコピーしておく sudo install -m 600 -o $USER -g $(id -g) /var/root/Library/Application\ Support/multipassd/ssh-keys/id_rsa ~/.ssh/multipass.id_rsa ~/.ssh/config に下記の設定を追記しておく(HostNameはmultipassで起動したubuntuのLocalIPに合わせる) Host ryujidev User ubuntu HostName 192.168.64.3 IdentityFile ~/.ssh/multipass.id_rsa これでmultipass shell だけでなく、 ssh ryujidev でubuntuにアクセスできるようになりました。 あとは下記を参考に vscodeのremote debug で ubuntuに接続し、src code本体は、仮想環境ubuntuにgitで取得して、UIは mac 上の vscodeで編集できます。 https://code.visualstudio.com/docs/remote/ssh#_connect-to-a-remote-host container のポートフォワード(WebServerのデバッグ等) https://code.visualstudio.com/docs/remote/ssh#_forwarding-a-port-creating-ssh-tunnel docker, k8s の動作確認 以下のように上記vscode remote debugで 仮想 ubuntuにつなぎ、vscodeのターミナルで、kind, k8s をたちあげてistioctl で kialiを開いてみます。(istioctlについての参考:https://qiita.com/kozayupapa/items/dfbabe1bd6eb40f4ddf9) 上記で実行するとvscode ポートタブに移動すると port forward設定が自動で追加しれてくれています!(便利!!かゆいところにてがとどいているさすがMicrosoft) MacのBrowserで localhost:20001 にアクセスするとちゃんと開いて確認できました。 これで無事 DockerDesktopを卒業できました。 今後また container CI/CDで利用することになったら再度Dockerファミリにお世話になろうと思います。 しばらくは AzureのContainerRepositoryを利用する予定なのでmultipassで十分かなと思いました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】学習メモ③~APIサーバの構築~

はじめに Go学習の一環として、簡単なAPIサーバを作成してみました。 その他のGoの記事については以下をご覧ください。 実装 DBにmovies, genres, movies_genresテーブルを作成し、指定したIDのmovieを取得するようなAPIをつくります。 movies genres movie_genres 導入 まずは以下のコマンドで外部からインポートするモジュールのパスを書いておくファイルを作成します。 go mod init backend 次に今回使う外部モジュール(httprouterとpq)をインストールします。 go get -u github.com/julienschmidt/httprouter go get -u github.com/lib/pq@v1.10.0 main.goの作成 main.goにDBとサーバを接続してlistenする部分を記述します(詳細はインラインコメント)。 main.go package main import ( "backend/models" "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const version = "1.0.0" type config struct { port int env string db struct { dsn string } } type AppStatus struct { Status string `json:"status"` Environment string `json:"environment"` Version string `json:"version"` } type application struct { config config logger *log.Logger models models.Models } func main() { var cfg config // flagでconfigのプロパティを初期化する // 引数は変数のポインタ(メモリのアドレス値)、フラグの名前、デフォルト値、使い方の説明 flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on") flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production") flag.StringVar(&cfg.db.dsn, "dsn", "postgres://localhost/go_movies?sslmode=disable", "Postgres connection string") flag.Parse() // Loggerオブジェクトを生成して出力フォーマットを設定する logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) // DBと接続する db, err := openDB(cfg) if err != nil { logger.Fatal(err) } // main()がreturnするときにDBとの接続を閉じる defer db.Close() // アプリケーションの設定をする(参照渡しをおこなうためにポインタを使用) app := &application{ config: cfg, logger: logger, models: models.NewModels(db), } // サーバー設定をカスタマイズする srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: 10 * time.Minute, WriteTimeout: 30 * time.Second, } logger.Println("Starting server on port", cfg.port) // サーバをlistenする err = srv.ListenAndServe() if err != nil { log.Println(err) } } func openDB(cfg config) (*sql.DB, error) { // DBへアクセスする(接続はまだ確立されない) db, err := sql.Open("postgres", cfg.db.dsn) // エラー処理 if err != nil { return nil, err } // 5sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) // openDB()がreturnするまで実行されない defer cancel() // DBとの接続を検証する err = db.PingContext(ctx) //エラー処理 if err != nil { return nil, err } return db, nil } models.goの作成 models.goを作成し、main.goで定義したapplicationのプロパティであるmodelsの構造体Models、dbを元にModelsを返すNewModels()、各テーブルの構造体Movie Genre MovieGenreを記述します。 models.go package models import ( "database/sql" "time" ) // Models is the wrapper for database type Models struct { DB DBModel } // NewModels returns models with db pool func NewModels(db *sql.DB) Models { return Models{ DB: DBModel{DB: db}, // DBModel{db} } } type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` MovieGenre map[int]string `json:"genres"` } type Genre struct { ID int `json:"-"` GenreName string `json:"genre_name"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } type MovieGenre struct { ID int `json:"-"` MovieID int `json:"-"` GenreID int `json:"-"` Genre Genre `json:"genre"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } routes.goの作成 main.goのサーバ設定のカスタマイズでHandler: app.routes()と記述しましたが、この部分でルートハンドラーを設定しています。 導入部分でインストールしたhttprouterモジュールはここで使われます。 routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) // ルートハンドラーのレシーバ func (app *application) routes() *httprouter.Router { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) return router } router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)でメソッド、パス、処理を指定しています。 処理の部分についてはまた別のファイルmovie-handlers.goに記述します。 movie-handlers.goの作成 ルートハンドラーの処理getOneMovieを記述します。 ParamsFromContextでクエリパラメータを取得するために、ここでもhttprouterを使用します。 movie-handlers.go package main import ( "errors" "net/http" "strconv" "github.com/julienschmidt/httprouter" ) func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) { // クエリパラメータを取得する params := httprouter.ParamsFromContext(r.Context()) // paramsを文字列から整数に変換する id, err := strconv.Atoi(params.ByName("id")) //エラー処理 if err != nil { app.logger.Print(errors.New("invalid id parameter")) app.errorJSON(w, err) return } // 指定したidのデータを取得する movie, err := app.models.DB.Get(id) // レスポンスをJSONで返す err = app.writeJSON(w, http.StatusOK, movie, "movie") if err != nil { app.errorJSON(w, err) return } } movies-db.goの作成 movie-handlers.goのmovie, err := app.models.DB.Get(id)の部分で、指定したidのデータをDBから取得しています。 このGet()メソッドをmovies-db.goに記述します。 movies-db.go package models import ( "context" "database/sql" "time" ) type DBModel struct { DB *sql.DB } // 指定idのmovieかerrorを返すメソッド(DBModelのポインタレシーバ) func (m *DBModel) Get(id int) (*Movie, error) { // 3sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // 指定したIDのmoviesを取得するクエリ query := `select id, title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at from movies where id = $1 ` // 指定したidのmoviesを取得する(1行) row := m.DB.QueryRowContext(ctx, query, id) var movie Movie // クエリの結果をmovieに割り当てる err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Runtime, &movie.Rating, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, ) if err != nil { return nil, err } // 指定したmovie_idのgenresを取得するクエリ query = `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1 ` // 指定したmovie_idのgenresを取得する(複数行) rows, _ := m.DB.QueryContext(ctx, query, id) defer rows.Close() genres := make(map[int]string) for rows.Next() { var mg MovieGenre err := rows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } movie.MovieGenre = genres return &movie, nil } utilities.goの作成 movie-handlers.goでは、DBから取得したデータやエラーを取得したあとに、レスポンスをJSONで返します。 writeJSON()やerrorJSONといったメソッドをutilities.goに記述します。 utilities.go package main import ( "encoding/json" "net/http" ) func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, wrap string) error { // メモリを割り当てる wrapper := make(map[string]interface{}) // wrapキーの構造体にdataを代入する wrapper[wrap] = data // 構造体をJSONに変換する js, err := json.Marshal(wrapper) if err != nil { return err } // ヘッダーにContent-Typeを付加する w.Header().Set("Content-Type", "application/json") // ステータスコードとともにHTTP応答ヘッダーを返す w.WriteHeader(status) // ボディを返す w.Write(js) return nil } func (app *application) errorJSON(w http.ResponseWriter, err error) { type jsonError struct { Message string `json:"message"` } theError := jsonError { Message: err.Error(), } app.writeJSON(w, http.StatusBadRequest, theError, "error") } json.Marshal()を行う際、構造体タグを利用することで以下のように結果を制御することができます。 type Genre struct { ID int `json:"-"` // Marshal時に省略される GenreName string `json:"genre_name"` // Marshal時にjsonのキーが'genre_name'になる CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } 動作確認 go run cmd/api/*.goもしくはgo run ./cmd/apiを実行します。 http://localhost:4000/v1/movie/1にアクセスすると、以下のように結果が表示されます。 コード全文 以下においてあります。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】APIサーバの構築 ~データの読み取り(Read)編~

はじめに Go学習の一環として、簡単なAPIサーバを作成してみました。 その他のGoの記事については以下をご覧ください。 実装 DBにmovies, genres, movies_genresテーブルを作成し、指定したIDのmovieを取得するようなAPIをつくります。 movies genres movie_genres 導入 まずは以下のコマンドで外部からインポートするモジュールのパスを書いておくファイルを作成します。 go mod init backend 次に今回使う外部モジュール(httprouterとpq)をインストールします。 go get -u github.com/julienschmidt/httprouter go get -u github.com/lib/pq@v1.10.0 main.goの作成 main.goにDBとサーバを接続してlistenする部分を記述します(詳細はインラインコメント)。 main.go package main import ( "backend/models" "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const version = "1.0.0" type config struct { port int env string db struct { dsn string } } type AppStatus struct { Status string `json:"status"` Environment string `json:"environment"` Version string `json:"version"` } type application struct { config config logger *log.Logger models models.Models } func main() { var cfg config // flagでconfigのプロパティを初期化する // 引数は変数のポインタ(メモリのアドレス値)、フラグの名前、デフォルト値、使い方の説明 flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on") flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production") flag.StringVar(&cfg.db.dsn, "dsn", "postgres://localhost/go_movies?sslmode=disable", "Postgres connection string") flag.Parse() // Loggerオブジェクトを生成して出力フォーマットを設定する logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) // DBと接続する db, err := openDB(cfg) if err != nil { logger.Fatal(err) } // main()がreturnするときにDBとの接続を閉じる defer db.Close() // アプリケーションの設定をする(参照渡しをおこなうためにポインタを使用) app := &application{ config: cfg, logger: logger, models: models.NewModels(db), } // サーバー設定をカスタマイズする srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: 10 * time.Minute, WriteTimeout: 30 * time.Second, } logger.Println("Starting server on port", cfg.port) // サーバをlistenする err = srv.ListenAndServe() if err != nil { log.Println(err) } } func openDB(cfg config) (*sql.DB, error) { // DBへアクセスする(接続はまだ確立されない) db, err := sql.Open("postgres", cfg.db.dsn) // エラー処理 if err != nil { return nil, err } // 5sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) // openDB()がreturnするまで実行されない defer cancel() // DBとの接続を検証する err = db.PingContext(ctx) //エラー処理 if err != nil { return nil, err } return db, nil } models.goの作成 models.goを作成し、main.goで定義したapplicationのプロパティであるmodelsの構造体Models、dbを元にModelsを返すNewModels()、各テーブルの構造体Movie Genre MovieGenreを記述します。 models.go package models import ( "database/sql" "time" ) // Models is the wrapper for database type Models struct { DB DBModel } // NewModels returns models with db pool func NewModels(db *sql.DB) Models { return Models{ DB: DBModel{DB: db}, // DBModel{db} } } type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` MovieGenre map[int]string `json:"genres"` } type Genre struct { ID int `json:"-"` GenreName string `json:"genre_name"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } type MovieGenre struct { ID int `json:"-"` MovieID int `json:"-"` GenreID int `json:"-"` Genre Genre `json:"genre"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } routes.goの作成 main.goのサーバ設定のカスタマイズでHandler: app.routes()と記述しましたが、この部分でルートハンドラーを設定しています。 導入部分でインストールしたhttprouterモジュールはここで使われます。 routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) // ルートハンドラーのレシーバ func (app *application) routes() *httprouter.Router { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) return router } router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)でメソッド、パス、処理を指定しています。 処理の部分についてはまた別のファイルmovie-handlers.goに記述します。 movie-handlers.goの作成 ルートハンドラーの処理getOneMovieを記述します。 ParamsFromContextでクエリパラメータを取得するために、ここでもhttprouterを使用します。 movie-handlers.go package main import ( "errors" "net/http" "strconv" "github.com/julienschmidt/httprouter" ) func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) { // クエリパラメータを取得する params := httprouter.ParamsFromContext(r.Context()) // paramsを文字列から整数に変換する id, err := strconv.Atoi(params.ByName("id")) //エラー処理 if err != nil { app.logger.Print(errors.New("invalid id parameter")) app.errorJSON(w, err) return } // 指定したidのデータを取得する movie, err := app.models.DB.Get(id) // レスポンスをJSONで返す err = app.writeJSON(w, http.StatusOK, movie, "movie") if err != nil { app.errorJSON(w, err) return } } movies-db.goの作成 movie-handlers.goのmovie, err := app.models.DB.Get(id)の部分で、指定したidのデータをDBから取得しています。 このGet()メソッドをmovies-db.goに記述します。 movies-db.go package models import ( "context" "database/sql" "time" ) type DBModel struct { DB *sql.DB } // 指定idのmovieかerrorを返すメソッド(DBModelのポインタレシーバ) func (m *DBModel) Get(id int) (*Movie, error) { // 3sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // 指定したIDのmoviesを取得するクエリ query := `select id, title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at from movies where id = $1 ` // 指定したidのmoviesを取得する(1行) row := m.DB.QueryRowContext(ctx, query, id) var movie Movie // クエリの結果をmovieに割り当てる err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Runtime, &movie.Rating, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, ) if err != nil { return nil, err } // 指定したmovie_idのgenresを取得するクエリ query = `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1 ` // 指定したmovie_idのgenresを取得する(複数行) rows, _ := m.DB.QueryContext(ctx, query, id) defer rows.Close() genres := make(map[int]string) for rows.Next() { var mg MovieGenre err := rows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } movie.MovieGenre = genres return &movie, nil } utilities.goの作成 movie-handlers.goでは、DBから取得したデータやエラーを取得したあとに、レスポンスをJSONで返します。 writeJSON()やerrorJSONといったメソッドをutilities.goに記述します。 utilities.go package main import ( "encoding/json" "net/http" ) func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, wrap string) error { // メモリを割り当てる wrapper := make(map[string]interface{}) // wrapキーの構造体にdataを代入する wrapper[wrap] = data // 構造体をJSONに変換する js, err := json.Marshal(wrapper) if err != nil { return err } // ヘッダーにContent-Typeを付加する w.Header().Set("Content-Type", "application/json") // ステータスコードとともにHTTP応答ヘッダーを返す w.WriteHeader(status) // ボディを返す w.Write(js) return nil } func (app *application) errorJSON(w http.ResponseWriter, err error) { type jsonError struct { Message string `json:"message"` } theError := jsonError { Message: err.Error(), } app.writeJSON(w, http.StatusBadRequest, theError, "error") } json.Marshal()を行う際、構造体タグを利用することで以下のように結果を制御することができます。 type Genre struct { ID int `json:"-"` // Marshal時に省略される GenreName string `json:"genre_name"` // Marshal時にjsonのキーが'genre_name'になる CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } 動作確認 go run cmd/api/*.goもしくはgo run ./cmd/apiを実行します。 http://localhost:4000/v1/movie/1にアクセスすると、以下のように結果が表示されます。 CORS(Cross-Origin Resource Sharing) 別のオリジン(ドメイン、プロトコル、ポート番号)で提供されているフロントエンドのプロジェクト(React, Vue)からGoのAPIサーバにリクエストを行うためには、CORS(オリジン間リソース共有)を行う必要があります。 CORSを行うことによって、追加のHTTPヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにあるリソースへのアクセス権を与えるようブラウザに指示することができます。 HTTPヘッダーにAccess-Control-Allow-Originを付加するようなメソッドenableCORS()をmiddleware.goに作成して、routes.goに追記します。 middleware.go package main import ( "net/http" ) func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) }) } routes.go func (app *application) routes() http.Handler { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) return app.enableCORS(router) } コード全文 以下においてあります。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go】APIサーバを構築する① ~データの読み取り(Read)~

はじめに Go学習の一環として、簡単なAPIサーバを作成してみました。 その他のGoの記事については以下をご覧ください。 実装 DBにmovies, genres, movies_genresテーブルを作成し、指定したIDのmovieを取得するようなAPIをつくります。 movies genres movie_genres 導入 まずは以下のコマンドで外部からインポートするモジュールのパスを書いておくファイルを作成します。 go mod init backend 次に今回使う外部モジュール(httprouterとpq)をインストールします。 go get -u github.com/julienschmidt/httprouter go get -u github.com/lib/pq@v1.10.0 main.goの作成 main.goにDBとサーバを接続してlistenする部分を記述します(詳細はインラインコメント)。 main.go package main import ( "backend/models" "context" "database/sql" "flag" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) const version = "1.0.0" type config struct { port int env string db struct { dsn string } } type AppStatus struct { Status string `json:"status"` Environment string `json:"environment"` Version string `json:"version"` } type application struct { config config logger *log.Logger models models.Models } func main() { var cfg config // flagでconfigのプロパティを初期化する // 引数は変数のポインタ(メモリのアドレス値)、フラグの名前、デフォルト値、使い方の説明 flag.IntVar(&cfg.port, "port", 4000, "Server port to listen on") flag.StringVar(&cfg.env, "env", "development", "Application environment (development|production") flag.StringVar(&cfg.db.dsn, "dsn", "postgres://localhost/go_movies?sslmode=disable", "Postgres connection string") flag.Parse() // Loggerオブジェクトを生成して出力フォーマットを設定する logger := log.New(os.Stdout, "", log.Ldate|log.Ltime) // DBと接続する db, err := openDB(cfg) if err != nil { logger.Fatal(err) } // main()がreturnするときにDBとの接続を閉じる defer db.Close() // アプリケーションの設定をする(参照渡しをおこなうためにポインタを使用) app := &application{ config: cfg, logger: logger, models: models.NewModels(db), } // サーバー設定をカスタマイズする srv := &http.Server{ Addr: fmt.Sprintf(":%d", cfg.port), Handler: app.routes(), IdleTimeout: 10 * time.Minute, WriteTimeout: 30 * time.Second, } logger.Println("Starting server on port", cfg.port) // サーバをlistenする err = srv.ListenAndServe() if err != nil { log.Println(err) } } func openDB(cfg config) (*sql.DB, error) { // DBへアクセスする(接続はまだ確立されない) db, err := sql.Open("postgres", cfg.db.dsn) // エラー処理 if err != nil { return nil, err } // 5sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) // openDB()がreturnするまで実行されない defer cancel() // DBとの接続を検証する err = db.PingContext(ctx) //エラー処理 if err != nil { return nil, err } return db, nil } models.goの作成 models.goを作成し、main.goで定義したapplicationのプロパティであるmodelsの構造体Models、dbを元にModelsを返すNewModels()、各テーブルの構造体Movie Genre MovieGenreを記述します。 models.go package models import ( "database/sql" "time" ) // Models is the wrapper for database type Models struct { DB DBModel } // NewModels returns models with db pool func NewModels(db *sql.DB) Models { return Models{ DB: DBModel{DB: db}, // DBModel{db} } } type Movie struct { ID int `json:"id"` Title string `json:"title"` Description string `json:"description"` Year int `json:"year"` ReleaseDate time.Time `json:"release_date"` Runtime int `json:"runtime"` Rating int `json:"rating"` MPAARating string `json:"mpaa_rating"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` MovieGenre map[int]string `json:"genres"` } type Genre struct { ID int `json:"-"` GenreName string `json:"genre_name"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } type MovieGenre struct { ID int `json:"-"` MovieID int `json:"-"` GenreID int `json:"-"` Genre Genre `json:"genre"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } routes.goの作成 main.goのサーバ設定のカスタマイズでHandler: app.routes()と記述しましたが、この部分でルートハンドラーを設定しています。 導入部分でインストールしたhttprouterモジュールはここで使われます。 routes.go package main import ( "net/http" "github.com/julienschmidt/httprouter" ) // ルートハンドラーのレシーバ func (app *application) routes() *httprouter.Router { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) return router } router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie)でメソッド、パス、処理を指定しています。 処理の部分についてはまた別のファイルmovie-handlers.goに記述します。 movie-handlers.goの作成 ルートハンドラーの処理getOneMovieを記述します。 ParamsFromContextでクエリパラメータを取得するために、ここでもhttprouterを使用します。 movie-handlers.go package main import ( "errors" "net/http" "strconv" "github.com/julienschmidt/httprouter" ) func (app *application) getOneMovie(w http.ResponseWriter, r *http.Request) { // クエリパラメータを取得する params := httprouter.ParamsFromContext(r.Context()) // paramsを文字列から整数に変換する id, err := strconv.Atoi(params.ByName("id")) //エラー処理 if err != nil { app.logger.Print(errors.New("invalid id parameter")) app.errorJSON(w, err) return } // 指定したidのデータを取得する movie, err := app.models.DB.Get(id) // レスポンスをJSONで返す err = app.writeJSON(w, http.StatusOK, movie, "movie") if err != nil { app.errorJSON(w, err) return } } movies-db.goの作成 movie-handlers.goのmovie, err := app.models.DB.Get(id)の部分で、指定したidのデータをDBから取得しています。 このGet()メソッドをmovies-db.goに記述します。 movies-db.go package models import ( "context" "database/sql" "time" ) type DBModel struct { DB *sql.DB } // 指定idのmovieかerrorを返すメソッド(DBModelのポインタレシーバ) func (m *DBModel) Get(id int) (*Movie, error) { // 3sでタイムアウトする ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() // 指定したIDのmoviesを取得するクエリ query := `select id, title, description, year, release_date, runtime, rating, mpaa_rating, created_at, updated_at from movies where id = $1 ` // 指定したidのmoviesを取得する(1行) row := m.DB.QueryRowContext(ctx, query, id) var movie Movie // クエリの結果をmovieに割り当てる err := row.Scan( &movie.ID, &movie.Title, &movie.Description, &movie.Year, &movie.ReleaseDate, &movie.Runtime, &movie.Rating, &movie.MPAARating, &movie.CreatedAt, &movie.UpdatedAt, ) if err != nil { return nil, err } // 指定したmovie_idのgenresを取得するクエリ query = `select mg.id, mg.movie_id, mg.genre_id, g.genre_name from movies_genres mg left join genres g on (g.id = mg.genre_id) where mg.movie_id = $1 ` // 指定したmovie_idのgenresを取得する(複数行) rows, _ := m.DB.QueryContext(ctx, query, id) defer rows.Close() genres := make(map[int]string) for rows.Next() { var mg MovieGenre err := rows.Scan( &mg.ID, &mg.MovieID, &mg.GenreID, &mg.Genre.GenreName, ) if err != nil { return nil, err } genres[mg.ID] = mg.Genre.GenreName } movie.MovieGenre = genres return &movie, nil } utilities.goの作成 movie-handlers.goでは、DBから取得したデータやエラーを取得したあとに、レスポンスをJSONで返します。 writeJSON()やerrorJSONといったメソッドをutilities.goに記述します。 utilities.go package main import ( "encoding/json" "net/http" ) func (app *application) writeJSON(w http.ResponseWriter, status int, data interface{}, wrap string) error { // メモリを割り当てる wrapper := make(map[string]interface{}) // wrapキーの構造体にdataを代入する wrapper[wrap] = data // 構造体をJSONに変換する js, err := json.Marshal(wrapper) if err != nil { return err } // ヘッダーにContent-Typeを付加する w.Header().Set("Content-Type", "application/json") // ステータスコードとともにHTTP応答ヘッダーを返す w.WriteHeader(status) // ボディを返す w.Write(js) return nil } func (app *application) errorJSON(w http.ResponseWriter, err error) { type jsonError struct { Message string `json:"message"` } theError := jsonError { Message: err.Error(), } app.writeJSON(w, http.StatusBadRequest, theError, "error") } json.Marshal()を行う際、構造体タグを利用することで以下のように結果を制御することができます。 type Genre struct { ID int `json:"-"` // Marshal時に省略される GenreName string `json:"genre_name"` // Marshal時にjsonのキーが'genre_name'になる CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } 動作確認 go run cmd/api/*.goもしくはgo run ./cmd/apiを実行します。 http://localhost:4000/v1/movie/1にアクセスすると、以下のように結果が表示されます。 CORS(Cross-Origin Resource Sharing) 別のオリジン(ドメイン、プロトコル、ポート番号)で提供されているフロントエンドのプロジェクト(React, Vue)からGoのAPIサーバにリクエストを行うためには、CORS(オリジン間リソース共有)を行う必要があります。 CORSを行うことによって、追加のHTTPヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにあるリソースへのアクセス権を与えるようブラウザに指示することができます。 HTTPヘッダーにAccess-Control-Allow-Originを付加するようなメソッドenableCORS()をmiddleware.goに作成して、routes.goに追記します。 middleware.go package main import ( "net/http" ) func (app *application) enableCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") next.ServeHTTP(w, r) }) } routes.go func (app *application) routes() http.Handler { router := httprouter.New() router.HandlerFunc(http.MethodGet, "/v1/movie/:id", app.getOneMovie) return app.enableCORS(router) } コード全文 以下においてあります。 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Golang】go path / go get / go install / go mod tidyについて

はじめに Go1.17のエコシステムをまとめました。 意外に情報が少なく、私がGoを学ぶ上で最初に苦戦した領域でした。 もし間違いがあれば優しくご指摘いただけると幸いです>< GOPATHについて GOPAHTのデフォルトは~/go。この下に src/ bin/ pkg/という3つのディレクトリがある。 bin/ mainパッケージをビルドしたバイナリーファイルが格納される。コマンドとして実行できる。 (mainパッケージはビルドするとコマンドになる!!コマンド=実行可能なファイル) src/ Goのソースコードが格納される。かつてはこのディレクトリ以下で開発する必要があった。GitHubからダウンロードしたライブラリもかつてはここに配置されていた。 pkg/ ダウンロードしたパッケージのソースコード(ビルド前)が格納されるディレクトリ。コマンド以外(つまりmain pacakge以外)のビルド済のバイナリーファイルが置かれる場所でもある。mainパッケージ以外の名前のパッケージをビルドするとhoge.aというバイナリーファイルになり、importによってソースコードから使用できる。 GOPATHモード vs モジュール対応モード GOPATHモードとは 標準ライブラリを除く全てのパッケージのコード管理とビルドを環境変数GOPATHで指定されたディレクトリ下で行う。 モジュール対応モードとは 標準ライブラリを除く全てのパッケージをモジュールとして管理する。コード管理とビルドは任意のディレクトリで可能。Go1.17からは常にコレ。 モジュールとは? パッケージ: 1つのファイルを複数に分割したもの。1ディレクトリ=1パッケージ。 モジュール: go.modファイルのあるディレクトリ以下の全てのパッケージ(go.modは含まれない)。1レポジトリ=1モジュール importの仕方 GOPATHモード: import path/to/pacakge (物理パスsrc/の下から探す) モジュール対応モード: import github.com/gorilla/mux (レポジトリ名) もしGOPATHモードで import github.com/gorilla/mux と書くと、~/go/src/github.com/gorilla/muxを探す。 GOPATHモードとモジュールモードの切り替え 環境変数GO111MODULEで2つのモードを切り替えることができる。 GO111MODULE=on => 常にモジュール対応モード GO111MODULE=off => 常にGOPATHモード Go1.17ではGO111MODULE=onがデフォルト値。 モジュールのインストール先 モジュール対応モードでのインストール先は環境変数GOMODCACHEに保存される。 デフォルトはGOMODCACHE=~/go/pkg/mod(~/goがGOPAH) go.modとgo.sum モジュール管理に使うファイルである。 goで開発する時にgo.modは必ず必要なので、最初に初期化コマンドでgo.modを作る。 $ go mod init どちらも手動でいじるファイルではない。 go mod tidy go mod tidyはgo.modとgo.sumを更新する。 たとえば、main.goにimport "github.com/gorilla/mux"と書いてgo mod tidyをすると環境変数GOMODCACHEで指定されたディレクトリにmuxがダウンロードされ、go.modとgo.sumにmuxが追加され、muxが使えるようになる。 これがgo getを使わないライブラリのダウンロード方法である。 ちなみに、go.modとgo.sumがあってソースコード中にimportで呼び出してあれば、go run main.goの時にパッケージのインストールをGoが勝手にやってくれる。 go get vs go install go get :外部パッケージで、mainパッケージがないのもをダウンロードする時に使うコマンド。 go.modとgo.sumを変更してしまう。 go install:外部パッケージで、mainパッケージがあるものをダウンロード+ビルド+GOPATH/binへ移動するコマンド。(外部パッケージではないコードをビルド+mvする時にも使える)。mainパッケージのないリポジトリをgo installするとエラーになる。 GitHubからダウンロードする時 コマンド以外(mainパッケージではないもの) ソースコードにimport "github.com/hoge"を書く → $ go mod tidyを実行 → go.modとgo.sumに依存関係がか書かれてダウンロード成功 (go getを使っても良い) コマンド $ go install → $GOPATH/binにバイナリーファイルができる (go getは使わない)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む