20210418のGoに関する記事は3件です。

【Go】Stack & Queue (スタックとキュー)

Goでプログラミングの基礎を学ぶシリーズ スクールでは教えてくれないプログラミングの基礎、データ構造とアルゴリズムをGoで学んでいくシリーズです。 そのデータ構造がどのようなものであるかは、理解を助けてくれるサイトを紹介しつつ、簡単に説明に留めさせていただきます。(ご自身でも調べてみてください!) 筆者自身、Exerciseに取り組みながら理解を深めていったので、コードの解説を中心に記事を書いていきたいと思います。 タイトル #0 はじめに (環境構築と勉強方法) #1 Pointer, Array, String (ポインタと配列と文字列と) #2 File operations (ファイル操作) #3 Linked List (連結リスト) #4 Stack & Queue (スタックとキュー) ☜ here ~~ coming soon ~~ #5 Search algorithms (検索アルゴリズム) #6 Tree (木構造) #7 Sorting algorithms (ソートアルゴリズム) #8 String pattern matching algorithms (文字列検索アルゴリズム) #9 Mapping & Hashing (ハッシュテーブルなど) ※上記予告になりますので、2回に分けたりするかもしれないです... 今回は#4 Stack & Queue (スタックとキュー)です。 Stack とは, Queue とは Stack はデータを後入れ先出し( LIFO: Last In First Out )の構造で保持するものです。 Queue はデータを先入れ先出し( FIFO: First In First Out )の構造で保持するものです。 詳細な説明や、使いどころ、例題まで以下の記事を読めば、完全に理解できるかと思います。 この記事ではStackやQueueの説明は割愛し、Goでの実装を中心にしています。 はじめに、上の記事のStackとQueueの簡易実装例を、Goで実装してみました。 Exerciseは、Stackで実装する括弧列の整合性チェックと、QueueをDoubly Linked Listで実装した問題です。 おまけで、筆者がメンターに泣きつきながらなんとか実装した、逆ポーランド記法(Stack)の問題も載せておきます。 Stack の簡易実装例 package main import "fmt" var max int = 100000 type Stack struct { data []int } type StackI interface { isEmpty() bool isFull() bool push(int) pop() int } var _ StackI = &Stack{} func (s *Stack) isEmpty() bool { return len(s.data) == 0 } func (s *Stack) isFull() bool { return len(s.data) == max } func (s *Stack) push(n int) { if s.isFull() { fmt.Println("error: stack is full") return } s.data = append(s.data, n) } func (s *Stack) pop() int { if s.isEmpty() { fmt.Println("error: stack is empty") return 0 } last := len(s.data) - 1 popData := s.data[last] s.data = s.data[:last] return popData } func main() { var s *Stack = &Stack{} // Stackを初期化 s.push(3) // [] -> [3] s.push(5) // [3] -> [3, 5] s.push(7) // [3, 5] -> [3, 5, 7] fmt.Println(s.pop()) // [3, 5, 7] -> [3, 5] で 7 を出力 fmt.Println(s.pop()) // [3, 5] -> [3] で 5 を出力 s.push(9) // [3] -> [3, 9] s.push(11) // [3, 9] -> [3, 9, 11] s.pop() // [3, 9, 11] -> [3, 9] s.pop() // [3, 9] -> [3] s.pop() // [3] -> [] if s.isEmpty() { fmt.Println("empty") } else { fmt.Println("not empty") } } ☟ 出力結果 7 5 empty 参考記事に似せて実装してみました。 より簡単にするために、Sliceを使って実装しています。 Stackのサイズなど甘い実装ではありますが、初心者向けということで、簡略化しております。 interfaceの参考記事: 初心者に送りたいinterfaceの使い方[Golang] 【Go】基本文法⑥(インターフェース) Queue の簡易実装例 package main import "fmt" var max int = 10000 type Queue struct { data []int } type QueueI interface { isEmpty() bool isFull() bool enqueue(int) dequeue() int } var _ QueueI = &Queue{} func (q *Queue) isEmpty() bool { return len(q.data) == 0 } func (q *Queue) isFull() bool { return len(q.data) == max } func (q *Queue) enqueue(n int) { if q.isFull() { fmt.Println("error: queue is full") return } q.data = append(q.data, n) } func (q *Queue) dequeue() int { if q.isEmpty() { fmt.Println("error: stack is empty") return 0 } dequeueData := q.data[0] q.data = q.data[1:] return dequeueData } func main() { var q *Queue = &Queue{} q.enqueue(3) // [] -> [3] q.enqueue(5) // [3] -> [3, 5] q.enqueue(7) // [3, 5] -> [3, 5, 7] fmt.Println(q.dequeue()) // [3, 5, 7] -> [5, 7] で 3 を出力 fmt.Println(q.dequeue()) // [5, 7] -> [5] で 5 を出力 q.enqueue(9) // [7] -> [7, 9] q.enqueue(11) // [7, 9] -> [7, 9, 11] q.dequeue() // [7, 9, 11] -> [9, 11] q.dequeue() // [9, 11] -> [11] q.dequeue() // [11] -> [] if q.isEmpty() { fmt.Println("empty") } else { fmt.Println("not empty") } } ☟ 出力結果 3 5 empty こちらも簡単のため、headやtailを用いずSliceで実装しています。 ? Exercise (())(())((()())))()))() のような括弧列がすべてペアを作ることができるか(整合性が取れているか)確認するプログラムを実装しましょう。 ☟ 解答例 package main import "fmt" // 成立したペア用のstruct type Pair struct { begin int end int } type Stack struct { data []int } type StackI interface { isEmpty() bool push(int) pop() int } var _ StackI = &Stack{} func (s *Stack) isEmpty() bool { return len(s.data) == 0 } func (s *Stack) push(i int) { s.data = append(s.data, i) } func (s *Stack) pop() int { if s.isEmpty() { fmt.Println("error") return 0 } last := len(s.data) - 1 j := s.data[last] s.data = s.data[:last] return j } func check(brackets string) bool { fmt.Printf("\n\ncheck: %s\n", brackets) var s *Stack = &Stack{} // Stackを初期化 var pairs []Pair // 成立したペアを格納するSlice // 括弧列を一括弧ずつ処理する for i, b := range brackets { if fmt.Sprintf("%c", b) == "(" { // "(" のとき s.push(i) // "("の index を Stack に push } else { // ")" のとき if s.isEmpty() { fmt.Printf("error") // ペアになる "(" が Stack にないので error! return false } j := s.pop() // ペアが成立したので、 Stack から pop pairs = append(pairs, Pair{begin: j, end: i}) // 成立したペアの index を Sliceに格納 } } // 括弧列の処理が終了したとき、 Stack が空でなければ、ペアが成立しなかった index が残っているということ if !s.isEmpty() { fmt.Printf("too many (") return false } // 成立したペアを出力(おまけ) for _, pair := range pairs { fmt.Printf("(%d, %d)", pair.begin, pair.end) } return true } func main() { check("((()())())") // (2, 3)(4, 5)(1, 6)(7, 8)(0, 9) check("())") // error check("(()") // too many ( } ☟ 出力結果 check: ((()())()) (2, 3)(4, 5)(1, 6)(7, 8)(0, 9) check: ()) error check: (() too many ( 成立したペアの出力はおまけのようなものなので、なくてもよいと思います。 Stackのpushやpopは簡易実装例と同じです。 ポイントは(のときはStackにpush、)のときはStackからpopしてペアが成立した!と判定することでしょうか。 )のときにStackが空であれば、ペアが成立しないのでfalseを返します。 また、入力値の処理がすべて終了したときにStackが空でなければ )とペアにならなかった(があるということですので、falseを返します。 ? Exercise キャンセル待ちのお客様リストを想定します。 お客様の情報には、名前、電話番号、メールアドレスを登録します。 キャンセル待ちのお客様情報をリストに登録していきましょう。 キャンセルが出たら、一番最初に登録されたお客様の情報をリストから削除し、出力しましょう。 ※解答例ではDoubly Linked Listを用いて実装しますが、上記が実現できればどのような実装方法でも問題ないです。 ☟ 解答例 package main import ( "fmt" ) type Customer struct { name string telephone_number string email_address string } type Node struct { data *Customer prev *Node next *Node } type Queue struct { front *Node rear *Node } type QueueI interface { isEmpty() bool Enqueue(Customer) Dequeue() *Node } var _ QueueI = &Queue{} func (q *Queue) isEmpty() bool { return q.front == q.rear && q.front == nil } func (q *Queue) Enqueue(c Customer) { var node *Node = &Node{data: &c} if q.isEmpty() { // Queue が空のとき、 front も rear も node をさす q.front = node q.rear = node } else { // Queue が空ではないとき、 q.rear.next = node // リストの最後尾に node をリンクする node.prev = q.rear // node の prev はそれまでリスト最後尾だった Node になる q.rear = node // リストの最後尾を node にする } } func (q *Queue) Dequeue() *Node { if q.isEmpty() { fmt.Println("No one is waiting for cancellation") return nil } node := q.front // 取り除く Node q.front = q.front.next // リストの先頭の Node は先頭の次の Node になる if q.front != nil { // リストが空ではない q.front.prev = nil } else { // リストが空 q.rear = nil } fmt.Printf("\nSomeone canceled!! %s can now be booked.\n", node.data.name) return node } // キャンセル待ちリストの出力 func (q *Queue) Traverse() { node := q.front for i := 1; ; i++ { if node == nil { break } else { fmt.Printf("%d: %v\n", i, *node.data) node = node.next } } } func main() { var q *Queue = &Queue{} // Queue を初期化 var c1 *Customer = &Customer{name: "Sakura", telephone_number: "090-1234-5678", email_address: "sakura@example.com"} fmt.Printf("\n%s is waiting for cancellation\n", c1.name) q.Enqueue(*c1) // キャンセル待ちリストに追加 q.Traverse() var c2 *Customer = &Customer{name: "Kaede", telephone_number: "080-1234-5678", email_address: "kaede@example.com"} fmt.Printf("\n%s is waiting for cancellation\n", c2.name) q.Enqueue(*c2) q.Traverse() q.Dequeue() // キャンセルが出たので、キャンセル待ちリストの先頭を Dequeue する q.Traverse() var c3 *Customer = &Customer{name: "Yuzu", telephone_number: "070-1234-5678", email_address: "yuzu@example.com"} fmt.Printf("\n%s is waiting for cancellation\n", c3.name) q.Enqueue(*c3) q.Traverse() } ☟ 出力結果 Sakura is waiting for cancellation 1: {Sakura 090-1234-5678 sakura@example.com} Kaede is waiting for cancellation 1: {Sakura 090-1234-5678 sakura@example.com} 2: {Kaede 080-1234-5678 kaede@example.com} Someone canceled!! Sakura can now be booked. 1: {Kaede 080-1234-5678 kaede@example.com} Yuzu is waiting for cancellation 1: {Kaede 080-1234-5678 kaede@example.com} 2: {Yuzu 070-1234-5678 yuzu@example.com} ? おまけExercise Stackを使用して、中置記法 (Infix Notation) の式を逆ポーランド記法 (Reverse Polish Notation)へ変換するプログラムを実装しましょう。 【条件】 1. 式は、1桁の正の数(1~9)と4つの演算子(+, -, *, /)で構成されます。 2. 標準入力から中置記法の式を読み取り、それを逆ポーランド記法に変換し、出力します。 3. 変換した式を計算し、計算結果を出力します。 【例】 入力 : 3+5*4 出力 : 354*+ = 23 参考: THE POLISH NOTATION 逆ポーランド記法変換 (計算機) ☟ 解答例 package main import ( "fmt" "strconv" ) // 特に置き換える必要はありませんが、汎用性のため Element に置き換えています type Element string type Stack struct { data []Element } type StackI interface { isEmpty() bool Push(operator Element) Pop() Element Top() Element } var _ StackI = &Stack{} func (stack *Stack) isEmpty() bool { return len(stack.data) == 0 } func (stack *Stack) Push(operator Element) { stack.data = append(stack.data, operator) } func (stack *Stack) Pop() Element { top := stack.Top() stack.data = stack.data[:len(stack.data)-1] return top } func (stack *Stack) Top() Element { if stack.isEmpty() { return "" } return stack.data[len(stack.data)-1] } // 演算子であるかどうかの判定 func isOperator(s string) bool { operators := []string{"+", "-", "*", "/"} for _, o := range operators { if s == o { return true } } return false } func inStackPriority(s string) int { switch s { case "+", "-": return 1 case "*", "/": return 2 default: return 0 } } func inComingPriority(s string) int { switch s { case "+", "-": return 1 case "*", "/": return 2 default: return 0 } } func convertRPN(input string) string { var stack *Stack = &Stack{} // Stack の初期化, 演算子を Push/Pop していく var expression string // 出力する式 for _, v := range input { s := fmt.Sprintf("%c", v) if isOperator(s) { // 演算子のとき for { if inComingPriority(s) > inStackPriority(string(stack.Top())) { stack.Push(Element(s)) break } else { top := stack.Pop() expression += string(top) } } } else { // 正数のとき expression += s } } for { if stack.isEmpty() { break } else { top := stack.Pop() expression += string(top) } } return expression } func check(e error) { if e != nil { panic(e) } } // 計算 func operate(s string, a, b int) int { switch s { case "+": return b + a case "-": return b - a case "*": return b * a case "/": return b / a default: return 0 } } func calculateRPN(expRPN string) int { var stack *Stack = &Stack{} // Stack の初期化, 正数を Push/Pop していく for _, v := range expRPN { s := fmt.Sprintf("%c", v) if isOperator(s) { // 演算子のとき a, err := strconv.Atoi(string(stack.Pop())) check(err) b, err := strconv.Atoi(string(stack.Pop())) check(err) stack.Push(Element(fmt.Sprintf("%d", operate(s, a, b)))) // 計算結果を Stack に Push } else { // 正数のとき stack.Push(Element(s)) } } n, err := strconv.Atoi(string(stack.Pop())) check(err) return n } func main() { var input string fmt.Println("計算式を入力してください (1~9,+,-,*,/) 例: 3+5*4") fmt.Scanf("%s", &input) expRPN := convertRPN(input) fmt.Printf(expRPN) result := calculateRPN(expRPN) fmt.Printf(" = %d", result) } ☟ 出力結果 計算式を入力してください (1~9,+,-,*,/) 例: 3+5*4 7-3*4/2+1 734*2/-1+ = 2 わかりにくいのはPriorityのところですかね。 演算子には優先度があって、大きい方が先に計算されます(中置記法でも同じですね)。 StackのTopにある演算子の優先度と比較して大きい場合と小さい場合で、演算子をPushするか、StackからPopするか処理が異なります。 おわりに Exerciseの解答例はあくまで例なので、もっといい書き方あるよ!という方、ぜひコメントをお寄せください! 説明についても、筆者自身が初心者であるため、ご指摘や補足は大歓迎でございます。 株式会社Link Sportsでは、あらゆるスポーツを楽しむ人たちに送る、チームマネジメントアプリを開発しています。 未経験でも経験豊富なエンジニアの方でも、スポーツ好きなら活躍できる環境があります! 絶賛エンジニア募集中です! Wantedly ▶︎ https://www.wantedly.com/projects/324177 Green ▶︎ https://www.green-japan.com/job/82003 次回は、データ構造とアルゴリズム#5 Search algorithms (検索アルゴリズム)です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go ロギングライブラリ

Go でログを出力する時に色々方法がありすぎたので、調べたことを簡単にメモ 組み込みライブラリ https://golang.org/pkg/log/ 一番サクッとやるならこれ print 関数で palin text 出力なので構造化しづらくてきつい 構造化したい場合は printf で自分で整形して頑張るかたち ログレベルによるフィルタリング機能はないので本番運用に乗せるのは厳しい感じ デバッグ用にサクッと標準出力するのに使うとかかな log.Print("something happened") // => 2021/04/01 12:00:00 something happened log.Printf("something happened, %s", "nanika ga okimashita") // => 2021/04/01 12:00:00 something happened, nanika ga okimashita サードパーティ製ライブラリ サードパーティ製の Go のロギングライブラリはたくさんあって選べない... https://github.com/avelino/awesome-go#logging 以下、github star が多いものをいくつかピックアップ logrus https://github.com/Sirupsen/logrus かなりポピュラーらしく github の star も多い go の構造化ログライブラリの草分け的存在らしく、後続の Zerolog や Zap、 Apex などが思想を受け継いで開発されている 現在は枯れており、以下のように述べている Seeing weird case-sensitive problems? It's in the past been possible to import Logrus as both upper- and lower-case. Due to the Go package environment, this caused issues in the community and we needed a standard. Some environments experienced problems with the upper-case variant, so the lower-case was decided. Everything using logrus will need to use the lower-case: github.com/sirupsen/logrus. Any package that isn't, should be changed. v2 の開発予定はないらしいが、現行バージョンのメンテは今でも続いている フォーマットを指定でき、簡単に json 形式でログを出力できる ログレベルによるフィルタリングももちろんできるので、本番環境ではデバッグレベルは出力しないなどの設定が可能 logrus.Info("something happened") // => INFO[0000] something happened logrus.SetFormatter(&logrus.JSONFormatter{}) logrus.Info("something happened") // => {"level":"info","msg":"something happened","time":"2021-04-01T12:00:00+09:00"} logrus.WithFields(logrus.Fields{ "code": "001", "content": "何かが起きました", }).Info("something happened") // => {"code":"001","content":"何かが起きました","level":"info","msg":"something happened","time":"2021-04-01T12:00:00+09:00"} zap https://github.com/uber-go/zap uber が開発したライブラリで、高速なのが売り 独自の zap 構造体に依存し、interface{} 型に依存しないことで、reflection less な作りらしい sync.Pool で Buffer や Encoder を再利用できるので速いらしい grpc の middleware 郡にも組み込まれていて、intercepter でのロギングでも活躍するそう https://github.com/grpc-ecosystem/go-grpc-middleware/tree/master/logging/zap 高速な Logger とそれより少し低速な SugaredLogger がある シビアな速度パフォーマンスが求められる場合は Logger を使い、それ以外なら基本は SugaredLogger を使うで良さそう Logger だと渡すメッセージとして zap.String のように zap 構造体を毎回記述しないといけないので開発効率的にはあまり良くなさそう 処理の途中で(リコンパイルせずに)簡単に切り替えができるので、プロジェクト設定自体はカジュアルに決めちゃって良さそう logger, _ := zap.NewDevelopment() logger.Info("something happened") // => {"level":"info","ts":1618302530.16781,"caller":"example/main.go:24","msg":"something happened"} logger.Info("something happened", zap.String("code", "001"), zap.String("content", "何かが起きました"), ) // => {"level":"info","ts":1618302530.167862,"caller":"example/main.go:25","msg":"something happened","code":"001","content":"何かが起きました"} // SugaredLogger は低速な代わりに Zap 構造体に縛られない slogger := logger.Sugar() slogger.Info("something happened", "code", "001", "content", "何かが起きました", ) // => {"level":"info","ts":1618302530.167874,"caller":"example/main.go:31","msg":"something happenedcode001content何かが起きました"} glog https://github.com/golang/glog ラストコミットが2016年でメンテされてなさそう 見た感じ構造化はできなくて、plain text 出力っぽい フォーマットも固定でなぜか year が出力されない(容量節約?)ので、本番運用には難しそう 設定値を起動オプションで渡すという仕様がイマイチ -logtostderr をコマンドライン引数で渡して実行してみた結果が以下 flag をパースせずに使おうとすると怒られる glog.Info("something happened") // => ERROR: logging before flag.Parse: I0413 12:00:00.452402 47903 main.go:36] something happened flag.Parse() glog.Info("something happened") // => I0401 12:00:00.452492 47903 main.go:39] something happened seelog https://github.com/cihub/seelog 高機能なんだけど設定ファイルが XML 形式 最後のコミットが2017年とかなのでこれもメンテされてなさそう defer seelog.Flush() seelog.Info("something happened") // => 1618304287968713000 [Info] something happened const logConfig = `<seelog type="sync"> <outputs> <filter levels="trace,debug,info"> <console formatid="ltsv"/> </filter> <filter levels="warn,error,critical"> <console formatid="ltsv_error"/> </filter> </outputs> <formats> <format id="ltsv" format="time:%Date(2006-01-02T15:04:05.000Z07:00)%tlev:%l%tmsg:%Msg%n"/> <format id="ltsv_error" format="%EscM(31)time:%Date(2006-01-02T15:04:05.000Z07:00)%tlev:%l%tmsg:%Msg%EscM(0)%n"/> </formats> </seelog>` logger, _ := seelog.LoggerFromConfigAsBytes([]byte(logConfig)) seelog.ReplaceLogger(logger) logger.Info("something happened") // => time:2021-04-01T12:00:00.968+09:00 lev:i msg:something happened go-spew https://github.com/davecgh/go-spew readme にも書いてあるんだけど、デバッグ用ログライブラリらしい ベンチ取ってみた 手前味噌だけど一応ベンチマークを取ってみた 書き出し内容とかをそこまで厳密に揃えたわけではないので単純比較できないけど、それでも zap が桁違いに速い $ go test -bench . goos: darwin goarch: amd64 pkg: example.com/go-log-test BenchmarkOutputLog_builtin-8 162410 9434 ns/op BenchmarkOutputLog_zap-8 3314227 353 ns/op BenchmarkOutputLog_logrus-8 75792 13932 ns/op BenchmarkOutputLog_zerolog-8 140047 9740 ns/op ベンチで実行するのに使ったスクリプトを一応 package main import ( "io" "log" "os" "testing" "github.com/rs/zerolog" "github.com/sirupsen/logrus" "go.uber.org/zap" ) func BenchmarkOutputLog_builtin(b *testing.B) { file, _ := os.OpenFile("./output.log", os.O_APPEND|os.O_WRONLY, os.ModeAppend) log.SetOutput(io.Writer(file)) b.ResetTimer() for i := 0; i < b.N; i++ { log.Print("output log") } } func BenchmarkOutputLog_zap(b *testing.B) { c := zap.NewProductionConfig() c.OutputPaths = []string{"./output.log"} logger, _ := c.Build() b.ResetTimer() for i := 0; i < b.N; i++ { logger.Info("output log") } } func BenchmarkOutputLog_logrus(b *testing.B) { logger := logrus.New() logger.Out, _ = os.OpenFile("./output.log", os.O_APPEND|os.O_WRONLY, os.ModeAppend) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Info("output log") } } func BenchmarkOutputLog_zerolog(b *testing.B) { file, _ := os.OpenFile("./output.log", os.O_APPEND|os.O_WRONLY, os.ModeAppend) logger := zerolog.New(file) b.ResetTimer() for i := 0; i < b.N; i++ { logger.Info().Msg("output log") } } まとめ デファクトスタンダード的なものはないけど、無難なら logrus を使っておけば良さそう 速度パフォーマンスを気にするなら zap 正直以下の要素が満たされればそんなに悩む必要もない気がする 構造化 ログレベルによるフィルタリング 設定のファイル管理
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Docker, VSCode Remote Container, Air による Go 開発環境構築

はじめに 概要 Docker と VSCode Remote Container による Go の Web サーバ開発環境を構築する記事です。 ローカルに Go をインストールすることなく、 コード補完 コード変更があるたびに再ビルド(ライブリロード) の Go 開発環境を構築します。 リポジトリ この記事で作成する内容は GitHub に上げています。 背景 業務で Go を触ることとなり、勉強するにあたって手軽な Go の開発環境の構築方法を調べていました。 Docker を使った Go の開発環境を構築する方法はいくつか情報があったのですが、コード補完やフォーマッタを効かせるために結局ローカルに Go をインストールする必要があり、少し微妙でした。 また、簡単な Web サーバを書いていたのですが、ソースコードを修正するたびにいちいちビルドし直すのが面倒でした。 色々と試している中で、 ローカルに Go を入れないといけない問題: VSCode の拡張機能 Remote Container いちいちビルドし直す問題: Go のパッケージ Air で解決できることが分かり、本記事はその成果物です。 できるだけ手軽に、Go の Web サーバ開発環境を整えたい人向けの記事となっています。 開発環境の構築 動作環境 ディレクトリ構造 . | ├── .devcontaier/ | └── devcontainer.json ├── .air.toml ├── docker-compose.yml └── Dockerfile コンテナ設定 Dockerfile FROM golang:1.16.3-alpine ENV GO111MODULE on WORKDIR /go/src/app RUN apk update \ && apk add git \ && go install github.com/cosmtrek/air@latest \ && go get -u golang.org/x/tools/gopls \ github.com/ramya-rao-a/go-outline ここでは、Go 拡張機能で使用する ranguage server に必要な gopls や、ライブリロードに必要な Air のバイナリインストール&ビルドを行っています。 docker-compose.yml version: "3.8" services: web: build: context: . dockerfile: Dockerfile command: "air" tty: true stdin_open: true command: "air" volumes: - ./app:/go/src/app ports: - 8080:8080 security_opt: - apparmor:unconfined cap_add: - SYS_PTRACE ライブリロード設定 .air.toml で、ライブリロードの設定を行います。 今回は、公式のサンプルコードをそのまま拝借して使用します。 コンテナ接続設定 VSCode Remote Container 用の設定をします。 .devcontainer/.devcontainer.json { "name": "Go-Practice", "dockerComposeFile": [ "../docker-compose.yml", ], "service": "web", "workspaceFolder": "/go/src/app", "settings": { "terminal.integrated.shell.linux": "/bin/ash", "go.toolsManagement.checkForUpdates": "off", "go.gopath": "/go", "go.gocodeAutoBuild": true, "go.formatTool": "gofmt", "go.useLanguageServer": true, "editor.formatOnSave": false, "[go]": { "editor.formatOnSave": true } }, "extensions": [ "golang.go" ], } extensionsの部分で、ワークスペースに任意の VSCode 拡張をインストールすることができます。今回は、最低限の設定として Go の VSCode 拡張を入れています。 他にも、今回は軽量なalpineイメージを使用するので、 "terminal.integrated.shell.linux": "/bin/ash", の部分でワークスペースで使用するシェルにashを指定しています。 alpineでもbashを使いたい方は、イメージビルド時に bash を入れたりするなど、適宜カスタマイズしてください。 開発環境の起動 Docker イメージのビルド docker-compoase build コンテナの立ち上げ docker-compose up -d docker-compose logs -f webでコンテナのログを見ると、ライブリロードのairが立ち上がっていることがわかります。 web_1 | __ _ ___ web_1 | / /\ | | | |_) web_1 | /_/--\ |_| |_| \_ // live reload for Go apps, with Go web_1 | 開発コンテナ接続 Remote Container でコンテナに接続する方法はいくつかあります。以下のうちどれかを行います。(どれでもいい) 以下、全て VSCode での作業 [Cmd + Shift + P] > [reopen in container]を選択 VSCode の左下にある >< を押す > [reopen in container]を選択 左側のアイコン(Remote Explerer) > Containers > 該当のコンテナを選択 Docker のコンテナ内で、VSCode を開くことができました。 コーディングしてみる Go コンテナの中に入ることができたので、とりあえず Gin で Web サーバーを立てて動作を確認します。 パッケージのインストール ash go mod init プロジェクト名 で go.mod を作成したら、 ash go get -u github.com/gin-gonic/gin でginをインストール。 サーバー立ち上げ ash touch main.go main.go package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } 動作確認 localhost:8080/ping { "message": "pong" } ちゃんと動いてそうです。 ライブリロードの確認 コードを少し変更してみます。 main.go package main import "github.com/gin-gonic/gin" func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "pong", }) }) + r.GET("/hoge", func(c *gin.Context) { +    c.JSON(200, gin.H{ + "message": "fuga", + }) + }) r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") } 再度動作確認 web_1 | main.go has changed web_1 | building... web_1 | running... web_1 | [GIN-debug] GET /ping --> main.main.func1 (3 handlers) web_1 | [GIN-debug] GET /hoge --> main.main.func2 (3 handlers) localhost:8080/hoge { "message": "fuga" } ちゃんと変更が反映されていることがわかります。 おわりに 手軽な Go の開発環境構築として、Docker・Air・VSCode Remote Container を使用する方法を紹介させていただきました。 「DB コンテナも使いたい」となったらdocker-compose.ymlに DB コンテナの記述を加えたり、「もっといろいろワークスペースをカスタマイズしたい」となったら拡張機能をワークスペースに入れたりなど、色々と柔軟に拡張できると思います。 最後に、メリットデメリットをまとめておきます。 本記事で紹介する環境のメリット/デメリット メリット Docker, VSCode があれば作れる ローカルに Go をインストールせずに、コード補完等のサポートが効く コードを変更すると、自動でソースコードをリビルドしてくれる docker コマンドをいちいち流さなくても良い デメリット git 管理の構成を考えるのが難しい 1 コンテナにつき開ける VSCode のウィンドウが一つ 何らかのポートを開けっぱなしにするとき、VSCode のデバッグが使えない メリットもある一方、デメリットも多くあるなぁと筆者自身感じています。特にデバッグ構成については、Web サーバの開発であればライブリロードと相性が悪いです。 (以下、デバッグと Air が共存できない例) 変更があるたびにソースコードがビルドされ、ポート:8080で Gin の Web サーバが立ち上がっている。 ここで、VSCode でデバッグを行うと、デバッグのためにソースコードがビルドされる。 デバッグでのビルド時、同じくポート:8080で Gin をサーバを立ち上げようとするため、ポートの競合が発生し、デバッグモードにできない。 こちらについては、未だベストな解決方法が思いついておりません。結局、ライブリロードをやめてデバッグモードでの開発が良いのかなと思っています。 以上のメリット・デメリットを踏まえた上で、今回の環境構築を試していただければと思います。 もっと良い方法があるよ!等ありましたら、是非教えていただきたいです ?‍♂️ 参考資料
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む