20210503のGoに関する記事は7件です。

【Go】Tree (木構造)

Goでプログラミングの基礎を学ぶシリーズ スクールでは教えてくれないプログラミングの基礎、データ構造とアルゴリズムをGoで学んでいくシリーズです。 そのデータ構造がどのようなものであるかは、理解を助けてくれるサイトを紹介しつつ、簡単な説明に留めさせていただきます。(ご自身でも調べてみてください!) 筆者自身、Exerciseに取り組みながら理解を深めていったので、コードの解説を中心に記事を書いていきたいと思います。 タイトル #0 はじめに (環境構築と勉強方法) #1 Pointer, Array, String (ポインタと配列と文字列と) #2 File operations (ファイル操作) #3 Linked List (連結リスト) #4 Stack & Queue (スタックとキュー) #5 Search algorithms (探索アルゴリズム) #6 Tree (木構造) ☜ here #7 Sorting algorithms (ソートアルゴリズム) #8 String pattern matching algorithms (文字列探索アルゴリズム) 今回は#6 Tree (木構造)です。 Treeとは Tree (木構造) とは、読んで字の通り、木のような構造のデータ構造のことです。 Linked List (連結リスト) が線型構造であったのに対し、 Tree は階層構造をとることができます。 Wikipediaを参考に、主な用語をあげておきます。 英語 日本語 意味 child node 子ノード 木構造の各ノードは0個以上の子ノードを持つ parent node 親ノード 子ノードを持つノード root node 根ノード 親ノードを持たないノード。 根ノードは木構造の最上位にあるノードであり、1つの木構造に1つしか存在しない。 leaf node 葉ノード 子ノードを持たないノード。木構造の下位の末端にあるノードであり、1つの木構造に複数存在しうる。 internal node 内部ノード 子ノードを持つノード。根ノードと葉ノード以外のノードをさす。 sub tree 部分木 木構造の一部であり、それ自身も完全な木構造となっている部分をさす Binary Tree (二分木) Binary Tree は Tree の中でも、一つの node が持つ child node の数が 2以下 の Tree です。 左は Tree の構造と各名称を図示し、右は Node のリンクを図示しました。 ここから、Binary Tree でよく使われる重要な関数をみていきます。 IsEmptyTree func (t *Tree) IsEmptyTree() bool { return t.root == nil } Tree が空かどうかの判定なので、 root が nil かどうかを返します。 IsLeaf func IsLeaf(node *Node) bool { if node == nil { return true } return node.left == nil && node.right == nil } 引数で渡ってきた node が leaf であるかどうかを判定します。 ChildTree func (n *Node) ChildTree() *Tree { if n == nil { return nil } return &Tree{root: n} } ある node を root とする Tree を返します。 sub tree を1つの Tree として扱っているような感じですね。 CountNodes などの Recursive function (再帰関数) で使われます。 CountNodes func (t *Tree) CountNodes() int { if t == nil || t.root == nil { return 0 } if IsLeaf(t.root) { return 1 } return 1 + t.root.left.ChildTree().CountNodes() + t.root.right.ChildTree().CountNodes() } Tree を構成する node の数をカウントします。 return で、 count を +1 することと、 root.left および root.right がそれぞれ root となった Tree に対して、CountNodes をしています。 CountLeafs func (t *Tree) CountLeafs() int { if t == nil { return 0 } if IsLeaf(t.root) { return 1 } return t.root.left.ChildTree().CountLeafs() + t.root.right.ChildTree().CountLeafs() } leaf の数をカウントします。 CountNonLeafNodes func (t *Tree) CountNonLeafNodes() int { if t == nil { return 0 } if IsLeaf(t.root) { return 0 } return 1 + t.root.left.ChildTree().CountNonLeafNodes() + t.root.right.ChildTree().CountNonLeafNodes() } leaf ではない node の数をカウントします。 Binary Search Tree (二分探索木) 構造は Binary Tree と変わりませんが、 Left nodes の値 < root node の値 < Right nodes の値 という制約をもちます。 操作として、 Search (探索), Insert (挿入), Delete (削除) があります。 Search func (t *Tree) SearchNode(x Element) *Node { if t == nil { return nil } if t.root.data == x { return t.root } if t.root.data < x { return t.root.right.ChildTree().Search(x) } else { return t.root.left.ChildTree().Search(x) } } Insert func (t *Tree) InsertNode(x Element) { node := &Node{data: x} if t.root == nil { t.root = node } else { if t.root.data < x { if rightChild := t.root.right.ChildTree(); rightChild != nil { rightChild.InsertNode(x) } else { t.root.right = node } } else { if leftChild := t.root.left.ChildTree(); leftChild != nil { leftChild.InsertNode(x) } else { t.root.left = node } } } } Delete Delete は処理が少し複雑です。 Search や Insert と異なり、削除された node の位置によって、他の node を移動させなくてはならないからです。 詳細は上の参考サイトやコードを見ていただきたいのですが、3つに場合分けされます。 削除する node が leaf の場合 対象の node を削除するだけ 削除する node が child node を1つもつ場合 対象の node があったところに child node を移動させる 削除する node が child node を2つもつ場合 (以下どちらでもよい) 削除する node の Right sub tree の最小値を root に移動させる 削除する node の Left sub tree の最大値を root に移動させる func (t *Tree) DeleteNode(x Element) *Node { if t == nil { return nil } if t.root.data < x { // root が削除する対象の node ではない t.root.right = t.root.right.ChildTree().DeleteNode(x) return t.root } else if t.root.data > x { // root が削除する対象の node ではない t.root.left = t.root.left.ChildTree().DeleteNode(x) return t.root } else { // root が削除する対象の node である if IsLeaf(t.root) { // ① 削除する node が leaf (child node をもたない)のとき t.root = nil // 対象の node を削除するだけでよい } else if t.root.left == nil && t.root.right != nil { // ② 削除する node が child node を 1 つもつとき t.root = t.root.right // child node を root に移動させる } else if t.root.left != nil && t.root.right == nil { // ② 削除する node が child node を 1 つもつとき t.root = t.root.left // child node を root に移動させる } else { // ③ 削除する node が child node を 2 つもつとき rightMin := t.RightSubTree(x).FindMostLeft().data // 削除する node の Right sub tree の最小値を root に移動させる t.root.data = rightMin t.root.right = t.root.right.ChildTree().DeleteNode(rightMin) } return t.root } } func (t *Tree) RightSubTree(x Element) *Tree { if t == nil { return nil } if t.root.data == x { return t.root.right.ChildTree() } else if t.root.data < x { return t.root.right.ChildTree().RightSubTree(x) } else { return t.root.left.ChildTree().RightSubTree(x) } } func (t *Tree) FindMostLeft() *Node { if t == nil { return nil } if t.root.left == nil { return t.root } return t.root.left.ChildTree().FindMostLeft() } 以上をふまえて、Exerciseをやってみましょう。 ? Exercise 1~9 がランダムに並んだ配列から Binary Search Tree を実装しましょう。 node の数を出力しましょう。 データが 2 の node を探索しましょう。 データが 2 の node を削除しましょう。 最後に全ての node を削除しましょう。 2~5を実行後に node を出力しておきましょう。 (次の Tree Traversal の内容になります。) ☟ 解答例 package main import "fmt" type TreeI interface { CountNodes() int PrintTree() Search(x Element) *Node InsertNode(x Element) DeleteNode(x Element) *Node RightSubTree(x Element) *Tree FindMostLeft() *Node DeleteAllNodes() *Node } type Element int type Node struct { data Element left *Node right *Node } type Tree struct { root *Node } var _ TreeI = &Tree{} func IsLeaf(node *Node) bool { if node == nil { return true } return node.left == nil && node.right == nil } func (n *Node) ChildTree() *Tree { if n == nil { return nil } return &Tree{root: n} } func (t *Tree) CountNodes() int { if t == nil || t.root == nil { return 0 } if IsLeaf(t.root) { return 1 } return 1 + t.root.left.ChildTree().CountNodes() + t.root.right.ChildTree().CountNodes() } func (t *Tree) PrintTree() { if t == nil || t.root == nil { fmt.Printf("nothing!") return } if t.root.left != nil { t.root.left.ChildTree().PrintTree() } fmt.Printf("%d ", t.root.data) if t.root.right != nil { t.root.right.ChildTree().PrintTree() } } func (t *Tree) Search(x Element) *Node { if t == nil { return nil } if t.root.data == x { return t.root } if t.root.data < x { return t.root.right.ChildTree().Search(x) } else { return t.root.left.ChildTree().Search(x) } } func (t *Tree) InsertNode(x Element) { node := &Node{data: x} if t.root == nil { t.root = node } else { if t.root.data < x { if rightChild := t.root.right.ChildTree(); rightChild != nil { rightChild.InsertNode(x) } else { t.root.right = node } } else { if leftChild := t.root.left.ChildTree(); leftChild != nil { leftChild.InsertNode(x) } else { t.root.left = node } } } } func (t *Tree) DeleteNode(x Element) *Node { if t == nil { return nil } if t.root.data < x { t.root.right = t.root.right.ChildTree().DeleteNode(x) return t.root } else if t.root.data > x { t.root.left = t.root.left.ChildTree().DeleteNode(x) return t.root } else { if IsLeaf(t.root) { t.root = nil } else if t.root.left == nil && t.root.right != nil { t.root = t.root.right } else if t.root.left != nil && t.root.right == nil { t.root = t.root.left } else { rightMin := t.RightSubTree(x).FindMostLeft().data t.root.data = rightMin t.root.right = t.root.right.ChildTree().DeleteNode(rightMin) } return t.root } } func (t *Tree) RightSubTree(x Element) *Tree { if t == nil { return nil } if t.root.data == x { return t.root.right.ChildTree() } else if t.root.data < x { return t.root.right.ChildTree().RightSubTree(x) } else { return t.root.left.ChildTree().RightSubTree(x) } } func (t *Tree) FindMostLeft() *Node { if t == nil { return nil } if t.root.left == nil { return t.root } return t.root.left.ChildTree().FindMostLeft() } func (t *Tree) DeleteAllNodes() *Node { if t != nil { t.root.left = t.root.left.ChildTree().DeleteAllNodes() t.root.right = t.root.right.ChildTree().DeleteAllNodes() t.root = nil return t.root } return nil } func main() { tree := &Tree{} integers := []Element{5, 2, 3, 8, 9, 1, 4, 6, 7} for _, v := range integers { tree.InsertNode(v) } fmt.Printf("count: %d\n", tree.CountNodes()) tree.PrintTree() tree.DeleteNode(2) fmt.Printf("\nafter delete 2: ") tree.PrintTree() tree.DeleteAllNodes() fmt.Printf("\nafter delete all nodes: ") tree.PrintTree() } ☟ 出力結果 count: 9 1 2 3 4 5 6 7 8 9 after delete 2: 1 3 4 5 6 7 8 9 after delete all nodes: nothing! コードが長いので複雑に見えますが、出力結果の通り、Exerciseの前に記載したコードを組み合わせているだけです。 Tree Traversal Tree にある全ての node を1回ずつ体系的に調査する処理のことです。 Traversal にはいくつか方法があります。 参考: Wikipedia 木構造 (データ構造)#走査法 以下の記事がわかりやすいです。 Deep First Search (DFS) は日本語では 深さ優先探索 と呼ばれます。 簡単にいうと、1つの node からいけるところまで探索していき、探索できなくなったらそこから一番近い点に戻って、再び探索をする走査法です。 In-order Traversal はソートされた順序になっているのでよく使われます。 ※ データ構造としては Stack が使われますが、本記事では Recursive function で実装しています。 Breadth First Search (BFS) は日本語では 幅優先探索と呼ばれます。 探索を開始した地点から、深さが同じ node を浅い方から順番に探索をしていく走査法です。 各 Traversal を Go で実装すると以下のようになります。 DFS ではデータを出力しているタイミングが異なっていることが見てわかりますね。 Pre-order (前順 / 行きがけ順) func (t *Tree) PreorderTraversal() { if t == nil || t.root == nil { fmt.Printf("nothing!") return } fmt.Printf("%s ", t.root.data) if t.root.left != nil { t.root.left.ChildTree().PreorderTraversal() } if t.root.right != nil { t.root.right.ChildTree().PreorderTraversal() } } In-order (間順 / 通りがけ順) func (t *Tree) InorderTraversal() { if t == nil || t.root == nil { fmt.Printf("nothing!") return } if t.root.left != nil { t.root.left.ChildTree().InorderTraversal() } fmt.Printf("%s ", t.root.data) if t.root.right != nil { t.root.right.ChildTree().InorderTraversal() } } Post-order (後順 / 帰りがけ順) func (t *Tree) PostorderTraversal() { if t == nil || t.root == nil { fmt.Printf("nothing!") return } if t.root.left != nil { t.root.left.ChildTree().PostorderTraversal() } if t.root.right != nil { t.root.right.ChildTree().PostorderTraversal() } fmt.Printf("%s ", t.root.data) } Level-order (レベル順) func (t *Tree) LevelorderTraversal(queue []*Node) { if t == nil || t.root == nil { fmt.Printf("nothing!") return } if len(queue) == 0 { queue = append(queue, t.root) t.root.ChildTree().LevelorderTraversal(queue) } else { node := queue[0] queue = queue[1:] fmt.Printf("%s ", node.data) if node.left != nil { queue = append(queue, node.left) } if node.right != nil { queue = append(queue, node.right) } if len(queue) == 0 { return } queue[0].ChildTree().LevelorderTraversal(queue) } } Level-orderは他の Traversal と比較し、少しややこしいので流れを図にしました。 ? Exercise テキストファイルに入力されている文を読み取り、単語の出現頻度を出力するプログラムを実装しましょう。 ☟ ファイル例 sentence.txt A black black cat saw a very small mouse and a very scared mouse ☟ 出力例 A 3 AND 1 BLACK 2 CAT 1 MOUSE 2 SAW 1 SCARED 1 SMALL 1 VERY 2 ☟ 解答例 package main import ( "bufio" "fmt" "io" "os" "strings" ) type TreeI interface { WordCount(word string) PrintTree() } type Frequencies struct { word string count int } type Node struct { data *Frequencies left *Node right *Node } type Tree struct { root *Node } var _ TreeI = &Tree{} func (n *Node) ChildTree() *Tree { if n == nil { return nil } return &Tree{root: n} } // Tree を形成 func (t *Tree) WordCount(word string) { if t.root == nil { t.root = &Node{data: &Frequencies{word: word, count: 1}} } else { if t.root.data.word == word { t.root.data.count++ } else if t.root.data.word < word { if rightChild := t.root.right.ChildTree(); rightChild != nil { rightChild.WordCount(word) } else { t.root.right = &Node{data: &Frequencies{word: word, count: 1}} } } else { if leftChild := t.root.left.ChildTree(); leftChild != nil { leftChild.WordCount(word) } else { t.root.left = &Node{data: &Frequencies{word: word, count: 1}} } } } } // In-order Traversal // ソートされた順に単語と出現回数を出力する func (t *Tree) PrintTree() { if t == nil || t.root == nil { fmt.Printf("nothing!") return } if t.root.left != nil { t.root.left.ChildTree().PrintTree() } fmt.Printf("%s %d ", t.root.data.word, t.root.data.count) if t.root.right != nil { t.root.right.ChildTree().PrintTree() } } func check(e error) { if e != nil { if e != io.EOF { panic(e) } } } func main() { // ファイルから文を読み取って単語を抽出する f, err := os.Open("sentence.txt") check(err) defer f.Close() r := bufio.NewReader(f) sentence, err := r.ReadString('\n') check(err) sentence = strings.ToUpper(sentence) // 読み取った文に含まれれるアルファベットを全て大文字にする words := strings.Split(sentence, " ") // 単語ごとに区切って Slice に格納する // 単語で Tree を形成する tree := &Tree{} for _, w := range words { tree.WordCount(w) } // Tree を出力 tree.PrintTree() } ☟ 出力結果 A 3 AND 1 BLACK 2 CAT 1 MOUSE 2 SAW 1 SCARED 1 SMALL 1 VERY 2 形成された Tree は下図のようになっています。 おわりに Exerciseの解答例はあくまで例なので、もっといい書き方あるよ!という方、ぜひコメントをお寄せください! 説明についても、筆者自身が初心者であるため、ご指摘や補足は大歓迎でございます。 株式会社Link Sportsでは、あらゆるスポーツを楽しむ人たちに送る、チームマネジメントアプリを開発しています。 未経験でも経験豊富なエンジニアの方でも、スポーツ好きなら活躍できる環境があります! 絶賛エンジニア募集中です! Wantedly ▶︎ https://www.wantedly.com/projects/324177 Green ▶︎ https://www.green-japan.com/job/82003 次回は、データ構造とアルゴリズム#7 Sorting algorithms (ソートアルゴリズム)です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語】ホットリロード可能なHTTPサーバのDocker環境構築手順

先日、こちらの記事でAirを利用したホットリロード可能なHTTPサーバの紹介をしました。 今回は『Air + Go言語』のDocker環境を構築する手順について紹介します。 今回利用するサンプルコード localhost:3000にアクセスするとレスポンスが返ってくるHTTPサーバをサンプルとして利用します。 main.go package main import ( "log" "net/http" ) func rootHander(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "text/html; charset=utf8") w.Write([]byte("こんにちは")) } func main() { http.HandleFunc("/", rootHander) log.Fatal(http.ListenAndServe(":3000", nil)) } 『Air + Go言語』のDocker環境の構築手順 Docker環境の構築手順について紹介します。 下準備: 作業ディレクトリの用意 作業ディレクトリとgo.modファイルの作成をします。 $ mkdir go-docker-example && cd $_ $ go mod init `basename $PWD` 各種ファイルの作成 Dockerfileファイルは以下の通りです。 Dockerfile FROM golang:1.16.3-buster # コンテナの作業ディレクトリにローカルのファイルをコピー WORKDIR /app COPY . /app # 必要なパッケージをインストール RUN go mod tidy # Airをインストール RUN go install github.com/cosmtrek/air@v1.27.3 # airコマンドでGoファイルを起動 CMD ["air"] docker-compose.ymlは以下の通りです。 HTTPサーバがlistenしているポートと、開放するコンテナのポートは一緒にする必要があります。(今回でいうところの3000番) docker-compose.yml version: '3' services: app: build: . ports: - '3030:3000' # ローカルの3030番ポートでコンテナの3000番ポートに接続 volumes: - .:/app # ローカルとコンテナのディレクトリをバインドマウント(同期) - go_path:/go # パッケージやバイナリファイルのインストール先($GOPATH)を永続化 volumes: go_path: 動作確認 コンテナ起動後、ローカルからcurlを実行してレスポンスが返ってくればOKです。 ### 作業ディレクトリへ移動 $ cd go-example ### バックグラウンドで起動 $ dokcer-compose up -d ### 接続の確認 $ curl localhost:3030 こんにちは 参考: 依存パッケージ不足でコンテナ起動が失敗する問題を解決する 上記で紹介したDocerfileとdocker-compose.ymlでは、go.modに不備があるとコンテナ起動に失敗します。 たとえば、以下のようにコードを修正したとします。 main.go package main import ( "log" "net/http" + "rsc.io/quote" ) func rootHander(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "text/html; charset=utf8") - w.Write([]byte("こんにちは")) + w.Write([]byte(quote.Hello()) } func main() { http.HandleFunc("/", rootHander) log.Fatal(http.ListenAndServe(":3000", nil)) } Goファイル修正後go mod tidyを実行せずにコンテナを起動すると、rsc.io/quoteがgo.modに追加されていないため失敗します。 $ docker-compose up Creating air_app_1 ... done Attaching to air_app_1 app_1 | app_1 | __ _ ___ app_1 | / /\ | | | |_) app_1 | /_/--\ |_| |_| \_ , built with Go app_1 | app_1 | watching . app_1 | !exclude tmp app_1 | building... app_1 | main.go:7:2: no required module provides package rsc.io/quote; to add it: app_1 | go get rsc.io/quote app_1 | failed to build, error: exit status 1 go mod tidyをコンテナ起動時、つまりairコマンドの直前に実行することで、この問題が解決できます。 start.sh #!/bin/bash -eu go mod tidy air start.shを利用してコンテナを起動するようにdocker-compose.ymlを修正します。 docker-compose.yml version: '3' services: app: build: . ports: - '3030:3000' volumes: - .:/app - go_path:/go + command: ["./start.sh"] volumes: go_path: airコマンドの直前にgo mod tidyが実行されるようになったため、問題なくコンテナが起動できます。 $ docker-compose up Creating air_app_1 ... done Attaching to air_app_1 app_1 | go: finding module for package rsc.io/quote app_1 | go: downloading rsc.io/quote v1.5.2 app_1 | go: found rsc.io/quote in rsc.io/quote v1.5.2 app_1 | go: downloading rsc.io/sampler v1.3.0 app_1 | go: downloading golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c app_1 | app_1 | __ _ ___ app_1 | / /\ | | | |_) app_1 | /_/--\ |_| |_| \_ , built with Go app_1 | app_1 | watching . app_1 | !exclude tmp app_1 | building... app_1 | running... さいごに 認識違いや補足があればコメントいただけるとうれしいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のホットリロードツール『Air』でコードの修正を即時反映させる

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

go入門

go入門 install > sudo apt install golang > go version go version go1.13.8 linux/amd64 Helloworld hello.goを作成. package main import "fmt" func main() { fmt.Print("Hello World!\n") } > go run hello.go Hello World! 湯婆婆 yubaba.goを作成. package main import ( "bufio" "fmt" "math/rand" "os" "strings" "time" ) func main() { fmt.Println("契約書だよ.そこに名前を書きな.") stdin := bufio.NewScanner(os.Stdin) stdin.Scan() name := stdin.Text() fmt.Printf("フン.%sというのかい.贅沢な名だねぇ.\n", name) nameArray := strings.Split(name, "") rand.Seed(time.Now().UnixNano()) newname := nameArray[rand.Intn(len(nameArray))] fmt.Printf("今からお前の名前は%sだ.いいかい,%sだよ.わかったら返事をするんだ,%s!!\n", newname, newname, newname) } > go run yubaba.go 契約書だよ.そこに名前を書きな. hogehoge フン.hogehogeというのかい.贅沢な名だねぇ. 今からお前の名前はhだ.いいかい,hだよ.わかったら返事をするんだ,h!! 参考 Go言語入門 - とほほのWWW入門 (tohoho-web.com) go言語(golang)の特徴って?メリットとデメリットをまとめてみた | アトオシ (itpropartners.com) UbuntuでGoの開発環境をインストールしてHelloWorldしよう! - Qiita go run と go buildの違い - PONCOTSU (hatenablog.jp) Go 言語で湯婆婆を実装してみる - Qiita
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のTable Driven Testでt.parallel()によるテスト並行実行時、一部のテストケースしか評価されない問題について

概要 Gopher by Renée French, and tenntenn Go言語では、テストケースの入力値と期待値を分かりやすくする方法として、Table Driven Test(テーブル駆動テスト1)が推奨されています。 また、testingパッケージに用意されているt.parallel()メソッドを使用して、各テストを並行実行2させるといった手法も、テスト時間を短縮するためによく用いられています3。 Table Driven Testと*testing.Tのt.parallel()を組み合わせる方法は、結構あるあるかと思うのですが、テストを書く際に気を付けていないと、一部のテストケースしか評価されないという問題が発生してしまいます。 本記事は、上述した問題について どういった場合に発生するのか どうすれば防げるか なぜ発生するのか を調べ、まとめた内容になります。 環境 どういった場合に発生するのか 簡単な関数のテストを考えます。言語の名前が入ることを期待するstring型を引数にとり、Gopher(Go開発者)かどうかを判定する関数です。 programmer.go func IsGopher(language string) bool { return language == "GoLang" } Table Driven Testにのっとって、この関数のテストを書きます。 ここで、今回、テストケースの中には一つだけ失敗するパターンを仕込んであります。そのため、このテストは失敗します。 { // 失敗することを期待 name: "Rustaceanの場合、trueを期待する", args: args{ language: "Rust", }, want: true, }, テストコード全体 programmer_test.go func TestIsGopher(t *testing.T) { t.Parallel() type args struct { language string } tests := []struct { name string args args want bool }{ { name: "Gopherの場合、trueを期待する", args: args{ language: "GoLang", }, want: true, }, { // 失敗する name: "Rustaceanの場合、trueを期待する", args: args{ language: "Rust", }, want: true, }, { name: "PHPerの場合、falseを期待する", args: args{ language: "PHP", }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() if got := IsGopher(tt.args.language); got != tt.want { t.Errorf("IsGopher() = %v, want %v", got, tt.want) } }) } } このテストコードでは、以下のように、t.Run()によるサブテスト関数でt.Parallel()メソッドを呼びだし、サブテスト関数を並行に実行させるようにしています。 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() さて、テストを走らせます。一つだけ、Rustという文字列を渡してIsGopher関数がtrueを返すことを期待するテストケースが存在するため、テストは落ちるはずです。 しかし、実行すると、、 $ go test -v --- PASS: TestIsGopher (0.00s) --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s) --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s) --- PASS: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s) PASS ok テストが通ってしまいました。 なぜ発生するのか Table Driven Testでは、テストケースの構造体のスライスに対してforループを回し、各サブテスト関数を順次実行していきます。しかし、今回の例では、t.Parallel()を使用して、テストを並行に実行させるようにしています。 そのような場合、以下の挙動になります。 Go言語では、forステートメントはレキシカルブロックを作る。ループ内の関数値は同じ変数(ループ変数)を「補足」しており、特定の瞬間の値ではなくアドレスを共有している。 t.Parallel()呼び出しによって、並行実行されるまでサブテスト関数の実行は遅延される。 遅延したサブテスト関数が全て実行されるとき、テストケースの構造体変数ttは、forループの最後における値を保持している。 そのため、並行実行される全てのサブテスト関数値は"PHPerの場合、falseを期待する"になり、失敗するテストケースがあるのにもかかわらず、テストをパスしてしまう。 どうすれば防げるか forループの内側で、ttをコピーした変数を宣言することで防ぐことができます。 ループの内側でttを宣言し、外側のttで初期化することで、ループごとの値を保持させるようにします。 [補足]外側のttはループ変数、forブロック内で宣言したttはローカル変数です。スコープが異なるため、全く別物になります。サブテスト関数の引数には、ループ変数の値をコピーしたローカル変数を渡すようにします。 このとき、コピー先の変数名はコピー元と全く同じ名前を命名することが慣習となっています4。 for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() テスト実行 $ go test -v --- FAIL: TestIsGopher (0.00s) --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s) --- FAIL: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s) --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s) FAIL exit status 1 正しく動作させることができました。 まとめ Table Driven Testで正しくサブテストを並行実行させるためには、"遅延実行によって、forループ内のクロージャは同じ変数を評価する"というGo言語の特性を理解しておく必要があります。 まとめると以下の通りです。 Table Driven Testにおけるサブテスト実行時にt.Parallel()を呼び出すだけだと、ループ最後のテストケース構造体しかテストできない。 ループ内でループ変数を再宣言することにより、遅延したサブテスト関数の関数値が正しくテストケースを実行できるようになる。 追記 本記事を執筆中、他の方が同じ問題についての解決記事をご執筆されていることを知りました。直接的な問題の解決法についてはこちらの記事にお譲りし、本記事ではGoのループ変数に対するスコープ規則に絡めた、別アプローチでのアウトプット内容にしました。 間違い等ありましたら、コメント・編集リクエストでご指摘いただければ幸いです! 参考情報 https://github.com/golang/go/wiki/TableDrivenTests ↩ この記事における「並行」: Concurrent(並行)複数の動作が、論理的に、順不同もしくは同時に起こりうること(参考:https://qiita.com/yoshinori_hisakawa/items/486752636cf66225483a#%E4%B8%A6%E5%88%97%E5%87%A6%E7%90%86%E3%81%A8%E4%B8%A6%E8%A1%8C%E5%87%A6%E7%90%86%E3%81%AE%E9%81%95%E3%81%84%E3%81%A8%E3%81%AF) ↩ *testing.Tのt.parallelメソッドの詳しい内容については、他の方が簡潔に、かつ非常に分かりやすくまとめてくださっています。Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜 ↩ Alan A. A. Donovan and Brian W. Kernighan(柴田芳樹訳)(2016)『プログラミング言語Go』丸善出版,160-161。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語のTable Driven Testでt.parallel()によるテスト並行実行時、一部のテストケースしか評価されない問題について!

概要 Gopher by Renée French, and tenntenn Go言語では、テストケースの入力値と期待値を分かりやすくする方法として、Table Driven Test(テーブル駆動テスト1)が推奨されています。 また、testingパッケージに用意されているt.parallel()メソッドを使用して、各テストを並行実行2させるといった手法も、テスト時間を短縮するためによく用いられています3。 Table Driven Testと*testing.Tのt.parallel()を組み合わせる方法は、結構あるあるかと思うのですが、テストを書く際に気を付けていないと、一部のテストケースしか評価されないという問題が発生してしまいます。 本記事は、上述した問題について どういった場合に発生するのか どうすれば防げるか なぜ発生するのか を調べ、まとめた内容になります。 環境 どういった場合に発生するのか 簡単な関数のテストを考えます。言語の名前が入ることを期待するstring型を引数にとり、Gopher(Go開発者)かどうかを判定する関数です。 programmer.go func IsGopher(language string) bool { return language == "GoLang" } Table Driven Testにのっとって、この関数のテストを書きます。 ここで、今回、テストケースの中には一つだけ失敗するパターンを仕込んであります。そのため、このテストは失敗します。 { // 失敗することを期待 name: "Rustaceanの場合、trueを期待する", args: args{ language: "Rust", }, want: true, }, テストコード全体 programmer_test.go func TestIsGopher(t *testing.T) { t.Parallel() type args struct { language string } tests := []struct { name string args args want bool }{ { name: "Gopherの場合、trueを期待する", args: args{ language: "GoLang", }, want: true, }, { // 失敗する name: "Rustaceanの場合、trueを期待する", args: args{ language: "Rust", }, want: true, }, { name: "PHPerの場合、falseを期待する", args: args{ language: "PHP", }, want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() if got := IsGopher(tt.args.language); got != tt.want { t.Errorf("IsGopher() = %v, want %v", got, tt.want) } }) } } このテストコードでは、以下のように、t.Run()によるサブテスト関数でt.Parallel()メソッドを呼びだし、サブテスト関数を並行に実行させるようにしています。 for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() さて、テストを走らせます。一つだけ、Rustという文字列を渡してIsGopher関数がtrueを返すことを期待するテストケースが存在するため、テストは落ちるはずです。 しかし、実行すると、、 $ go test -v --- PASS: TestIsGopher (0.00s) --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s) --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s) --- PASS: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s) PASS ok テストが通ってしまいました。 なぜ発生するのか Table Driven Testでは、テストケースの構造体のスライスに対してforループを回し、各サブテスト関数を順次実行していきます。しかし、今回の例では、t.Parallel()を使用して、テストを並行に実行させるようにしています。 そのような場合、以下の挙動になります。 Go言語では、forステートメントはレキシカルブロックを作る。ループ内の関数値は同じ変数(ループ変数)を「補足」しており、特定の瞬間の値ではなくアドレスを共有している。 t.Parallel()呼び出しによって、並行実行されるまでサブテスト関数の実行は遅延される。 遅延したサブテスト関数が全て実行されるとき、テストケースの構造体変数ttは、forループの最後における値を保持している。 そのため、並行実行される全てのサブテスト関数値は"PHPerの場合、falseを期待する"になり、失敗するテストケースがあるのにもかかわらず、テストをパスしてしまう。 どうすれば防げるか forループの内側で、ttをコピーした変数を宣言することで防ぐことができます。 ループの内側でttを宣言し、外側のttで初期化することで、ループごとの値を保持させるようにします。 [補足]外側のttはループ変数、forブロック内で宣言したttはローカル変数です。スコープが異なるため、全く別物になります。サブテスト関数の引数には、ループ変数の値をコピーしたローカル変数を渡すようにします。 このとき、コピー先の変数名はコピー元と全く同じ名前を命名することが慣習となっています4。 for _, tt := range tests { + tt := tt t.Run(tt.name, func(t *testing.T) { // サブテスト関数を並行実行させる t.Parallel() テスト実行 $ go test -v --- FAIL: TestIsGopher (0.00s) --- PASS: TestIsGopher/Gopherの場合、trueを期待する (0.00s) --- FAIL: TestIsGopher/Rustaceanの場合、trueを期待する (0.00s) --- PASS: TestIsGopher/PHPerの場合、falseを期待する (0.00s) FAIL exit status 1 正しく動作させることができました。 まとめ Table Driven Testで正しくサブテストを並行実行させるためには、"遅延実行によって、forループ内のクロージャは同じ変数を評価する"というGo言語の特性を理解しておく必要があります。 まとめると以下の通りです。 Table Driven Testにおけるサブテスト実行時にt.Parallel()を呼び出すだけだと、ループ最後のテストケース構造体しかテストできない。 ループ内でループ変数を再宣言することにより、遅延したサブテスト関数の関数値が正しくテストケースを実行できるようになる。 追記 本記事を執筆中、他の方が同じ問題についての解決記事をご執筆されていることを知りました。直接的な問題の解決法についてはこちらの記事にお譲りし、本記事ではGoのループ変数に対するスコープ規則に絡めた、別アプローチでのアウトプット内容にしました。 間違い等ありましたら、コメント・編集リクエストでご指摘いただければ幸いです! 参考情報 https://github.com/golang/go/wiki/TableDrivenTests ↩ この記事における「並行」: Concurrent(並行)複数の動作が、論理的に、順不同もしくは同時に起こりうること(参考:https://qiita.com/yoshinori_hisakawa/items/486752636cf66225483a#%E4%B8%A6%E5%88%97%E5%87%A6%E7%90%86%E3%81%A8%E4%B8%A6%E8%A1%8C%E5%87%A6%E7%90%86%E3%81%AE%E9%81%95%E3%81%84%E3%81%A8%E3%81%AF) ↩ *testing.Tのt.parallelメソッドの詳しい内容については、他の方が簡潔に、かつ非常に分かりやすくまとめてくださっています。Go言語でのテストの並列化 〜t.Parallel()メソッドを理解する〜 ↩ Alan A. A. Donovan and Brian W. Kernighan(柴田芳樹訳)(2016)『プログラミング言語Go』丸善出版,160-161。 ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語でtailコマンドを作ってみる

Go言語でtailコマンドを作ってみる 最近Go言語を勉強し始めたが、その勉強の一環でtailコマンドをGo言語で作ってみる。 実装から、testingを用いたユニットテスト、GitHub Actionsを使ったCI/CDまでをやってみたいと思う。 バージョンアップなどにより、手順が異なる可能性があるので注意してください 流れ DockerによるGoの開発環境構築 マルチステージビルドの実現 大まかな仕様を考えてみる 実装 ユニットテスト テストカバレッジの確認 GitHub ActionsによるCI/CD(Go,Docker) 開発環境 macOS BigSur 11.2.3(20D91) Docker Desktop for Mac 20.10.5, build 55c4c88 DockerによるGoの開発環境構築 まず、今までGoを使ったことがないので、今後のことも考えて、DockerによるGoの開発環境を構築したい。 Dockerについては以前の記事を参考にしてください。 https://qiita.com/k_yuda/items/c9f48dbbdff70302c698 Dockerのインストール Docker for Macをインストール 公式サイトからDockerのアカウントを作ってログインし、DockerHubからダウンロードしてインストールする。 https://hub.docker.com/editions/community/docker-ce-desktop-mac インストール後CLIで確認してみる。 $ docker -v Docker version 20.10.5, build 55c4c88 このようにバージョンが表示されたら完了。 試しにubuntuを使ってみる。 まず、testフォルダー等を作ってDockerfileを作成する。 $ mkdir test $ cd test $ echo "From ubuntu" > Dockerfile 次にビルドして、shellに入ってみます。 $ docker build -t test . $ docker run -it test bash $ cat /etc/os-release 無事に起動できればこのようなメッセージが表示されると思います。 NAME="Ubuntu" VERSION="20.04.2 LTS (Focal Fossa)" これでDockerの動作を確認できました。 このコンテナは不要なので一旦キャッシュを含めて削除しましょう。 $ docker system prune -a マルチステージビルドの実現 名前だけ聞くと難しそうなイメージですが、簡単にまとめると 本来複数のDockerfileが必要な場合に対して、一つのDockerfileから複数のイメージBuildができる Fromをトリガーとして作用させ、複数ステージの生成物を継承できるということ ということです、メリットとしては、複数のコンテナを一つのDockerfileで管理できることです。 マルチステージビルドの設定の前に、まずファイル構成を決めましょう。 local gotail ├── .github │ └── workflows │ └── go.yml ├── README.md ├── Dockerfile ├── Makefile ├── main.go ├── main_test.go ├── test.txt ├── cmd.sh └── covercheck.sh 次にマルチステージビルドにおける、コンテナー内のファイル構成を考えてみる。 コンテナ1:go stage1 go └── src ├── main.go ├── main_test.go ├── test.txt └── cmd.sh コンテナ2:alpine linux stage2 root ├── main ├── test.txt └── cmd.sh 上記のようなファイル構成のマルチステージビルドを実現するには次のように記述します。 /local/Dockerfile FROM golang:latest WORKDIR /go/src COPY main.go . COPY main_test.go . COPY test.txt . COPY cmd.sh . RUN go test main_test.go main.go -v RUN go build main.go FROM alpine:latest RUN apk --no-cache add ca-certificates && \ apk add bash WORKDIR /root COPY --from=0 /go/src/main . COPY --from=0 /go/src/test.txt . COPY --from=0 /go/src/cmd.sh . CMD ["./cmd.sh"] 以上でマルチステージビルドは完了です。 詳しくは公式サイトをみてください。 https://docs.docker.com/develop/develop-images/multistage-build/ 大まかな仕様を決める 今回はtailコマンドのデフォルトの動作と、-nオプションの機能を実装したいと思う。 tailとは ファイルの最終行から数行を表示するコマンド、 標準では10行を表示する。 tail オプション -n 出力する行数を指定する また、-nオプションを実現するために、FIFOアルゴリズムを使って実装する。 FIFOとは FIFOとは、First In First Outの略称です。 FIFOを用いることによって、全てのデータをメモリに格納することなくファイルの読み込みが可能になります。 Wikiに詳しく載っています。 https://ja.wikipedia.org/wiki/FIFO このアルゴリズムを使ってtailを実現します。 使用するライブラリ コマンド本体のライブラリ一覧 main.go import ( "bufio" "flag" "fmt" "math" "os" ) testのライブラリ一覧 main_test.go import ( "bufio" "fmt" "os" "reflect" "strconv" "testing" ) まず、初めはイメージしやすいように細かく実装します。 キューの初期化 init_queue func init_queue() ([]string, int) { queue := []string{} cursor := 0 return queue, cursor } エンキュー enqueue func enqueue(queue []string, value string) []string { queue = append(queue, value) return queue } デキュー dequeue func dequeue(queue []string) []string { queue = queue[1:] return queue } キューの取り出し show_queue func show_queue(queue []string, n int) []string { if len(queue) == n { for i := n; i > 0; i-- { if len(queue) != 0 { fmt.Println(queue[0]) } queue = dequeue(queue) } } else { for i := len(queue); i > 0; i-- { if len(queue) != 0 { fmt.Println(queue[0]) } queue = dequeue(queue) } } return queue } 一連の流れをtailとして定義 tail func tail(stream *os.File, err error, n int) []string { queue, cursor := init_queue() scanner := bufio.NewScanner(stream) for scanner.Scan() { if n < 1 { n = int(math.Abs(float64(n))) if n == 0 { n = 10 } } queue = enqueue(queue, scanner.Text()) if n-1 < cursor { queue = dequeue(queue) } cursor++ } return queue } 実行してみるとこのような感じになります。※ここではわかりやすいようにqueueを表示しています。 test.txtには1~100の連番が一行ずつ入っているファイルです。 bash for i in `seq 100` do for> echo $i >> test.txt main.go $ go run main.go test.txt [1] [1 2] [1 2 3] [1 2 3 4] [1 2 3 4 5] [1 2 3 4 5 6] [1 2 3 4 5 6 7] [1 2 3 4 5 6 7 8] [1 2 3 4 5 6 7 8 9] [1 2 3 4 5 6 7 8 9 10] [1 2 3 4 5 6 7 8 9 10 11] [2 3 4 5 6 7 8 9 10 11 12] [3 4 5 6 7 8 9 10 11 12 13] [4 5 6 7 8 9 10 11 12 13 14] [5 6 7 8 9 10 11 12 13 14 15] [6 7 8 9 10 11 12 13 14 15 16] [7 8 9 10 11 12 13 14 15 16 17] [8 9 10 11 12 13 14 15 16 17 18] [9 10 11 12 13 14 15 16 17 18 19] [10 11 12 13 14 15 16 17 18 19 20] [11 12 13 14 15 16 17 18 19 20 21] [12 13 14 15 16 17 18 19 20 21 22] [13 14 15 16 17 18 19 20 21 22 23] [14 15 16 17 18 19 20 21 22 23 24] [15 16 17 18 19 20 21 22 23 24 25] [16 17 18 19 20 21 22 23 24 25 26] [17 18 19 20 21 22 23 24 25 26 27] [18 19 20 21 22 23 24 25 26 27 28] [19 20 21 22 23 24 25 26 27 28 29] [20 21 22 23 24 25 26 27 28 29 30] [21 22 23 24 25 26 27 28 29 30 31] [22 23 24 25 26 27 28 29 30 31 32] [23 24 25 26 27 28 29 30 31 32 33] [24 25 26 27 28 29 30 31 32 33 34] [25 26 27 28 29 30 31 32 33 34 35] [26 27 28 29 30 31 32 33 34 35 36] [27 28 29 30 31 32 33 34 35 36 37] [28 29 30 31 32 33 34 35 36 37 38] [29 30 31 32 33 34 35 36 37 38 39] [30 31 32 33 34 35 36 37 38 39 40] [31 32 33 34 35 36 37 38 39 40 41] [32 33 34 35 36 37 38 39 40 41 42] [33 34 35 36 37 38 39 40 41 42 43] [34 35 36 37 38 39 40 41 42 43 44] [35 36 37 38 39 40 41 42 43 44 45] [36 37 38 39 40 41 42 43 44 45 46] [37 38 39 40 41 42 43 44 45 46 47] [38 39 40 41 42 43 44 45 46 47 48] [39 40 41 42 43 44 45 46 47 48 49] [40 41 42 43 44 45 46 47 48 49 50] [41 42 43 44 45 46 47 48 49 50 51] [42 43 44 45 46 47 48 49 50 51 52] [43 44 45 46 47 48 49 50 51 52 53] [44 45 46 47 48 49 50 51 52 53 54] [45 46 47 48 49 50 51 52 53 54 55] [46 47 48 49 50 51 52 53 54 55 56] [47 48 49 50 51 52 53 54 55 56 57] [48 49 50 51 52 53 54 55 56 57 58] [49 50 51 52 53 54 55 56 57 58 59] [50 51 52 53 54 55 56 57 58 59 60] [51 52 53 54 55 56 57 58 59 60 61] [52 53 54 55 56 57 58 59 60 61 62] [53 54 55 56 57 58 59 60 61 62 63] [54 55 56 57 58 59 60 61 62 63 64] [55 56 57 58 59 60 61 62 63 64 65] [56 57 58 59 60 61 62 63 64 65 66] [57 58 59 60 61 62 63 64 65 66 67] [58 59 60 61 62 63 64 65 66 67 68] [59 60 61 62 63 64 65 66 67 68 69] [60 61 62 63 64 65 66 67 68 69 70] [61 62 63 64 65 66 67 68 69 70 71] [62 63 64 65 66 67 68 69 70 71 72] [63 64 65 66 67 68 69 70 71 72 73] [64 65 66 67 68 69 70 71 72 73 74] [65 66 67 68 69 70 71 72 73 74 75] [66 67 68 69 70 71 72 73 74 75 76] [67 68 69 70 71 72 73 74 75 76 77] [68 69 70 71 72 73 74 75 76 77 78] [69 70 71 72 73 74 75 76 77 78 79] [70 71 72 73 74 75 76 77 78 79 80] [71 72 73 74 75 76 77 78 79 80 81] [72 73 74 75 76 77 78 79 80 81 82] [73 74 75 76 77 78 79 80 81 82 83] [74 75 76 77 78 79 80 81 82 83 84] [75 76 77 78 79 80 81 82 83 84 85] [76 77 78 79 80 81 82 83 84 85 86] [77 78 79 80 81 82 83 84 85 86 87] [78 79 80 81 82 83 84 85 86 87 88] [79 80 81 82 83 84 85 86 87 88 89] [80 81 82 83 84 85 86 87 88 89 90] [81 82 83 84 85 86 87 88 89 90 91] [82 83 84 85 86 87 88 89 90 91 92] [83 84 85 86 87 88 89 90 91 92 93] [84 85 86 87 88 89 90 91 92 93 94] [85 86 87 88 89 90 91 92 93 94 95] [86 87 88 89 90 91 92 93 94 95 96] [87 88 89 90 91 92 93 94 95 96 97] [88 89 90 91 92 93 94 95 96 97 98] [89 90 91 92 93 94 95 96 97 98 99] [90 91 92 93 94 95 96 97 98 99 100] 91 92 93 94 95 96 97 98 99 100 次にイメージができたら、リファクタリングを行う 関数をできるだけまとめてみる。 tail func tail(stream *os.File, n int) []string { queue := []string{} scanner := bufio.NewScanner(stream) for scanner.Scan() { queue = append(queue, scanner.Text()) if n <= len(queue)-1 { queue = queue[1:] } } return queue } func show(queues []string) { for _, queue := range queues { fmt.Println(queue) } } 引数、flagを設定する mainにはファイルを読み込んだり、標準入力を読む処理や、オプションフラグをパースする処理を書きます。 複数ファイルを読み込む必要があるので、その処理も書きます。 main func main() { const USAGE string = "Usage: gotail [-n #] [file]" intOpt := flag.Int("n", 10, USAGE) flag.Usage = func() { fmt.Println(USAGE) } flag.Parse() n := int(math.Abs(float64(*intOpt))) if flag.NArg() > 0 { for i := 0; i < flag.NArg(); i++ { if i > 0 { fmt.Print("\n") } if flag.NArg() != 1 { fmt.Println("==> " + flag.Arg(i) + " <==") } fp, err := os.Open(flag.Arg(i)) if err != nil { fmt.Println("Error: No such file or directory") os.Exit(1) } defer fp.Close() show(tail(fp, n)) } } else { show(tail(os.Stdin, n)) } } ユニットテスト 次にテストを書く。 ここでは、あらかじめ用意しているtest.txtを読み込んで検証する。 また、ファイルに対して考えうるオプションを一通り試行できるようにテストする。 今回はtest.txtは100行の連番数字のデータで、-n 1 ~ -n 100まで順にテストしていく。 testTail func TestTail(t *testing.T) { var actual_all []string var expected_all []string count := 0 fp, err := os.Open("./test.txt") if err != nil { fmt.Println("Error: No such file or directory") os.Exit(1) } defer fp.Close() scanner := bufio.NewScanner(fp) for scanner.Scan() { count++ } n_all := 1 for i := count; i > 0; i-- { fp, err = os.Open("./test.txt") actual_all = tail(fp, n_all) expected_all = append([]string{strconv.Itoa(i)}, expected_all...) if reflect.DeepEqual(actual_all, expected_all) { t.Log(reflect.DeepEqual(actual_all, expected_all)) } else { t.Errorf("got %v\nwant %v", actual_all, expected_all) } n_all++ } } テストカバレッジの確認 また、テストカバレッジを確認する。 次のコマンドで確認できる。 ユーザーが使用できるリソースが制限されている場合があるので、 $ ulimit -aで確認する。必要に応じて$ ulimit -n 500等を実行しよう。 testcover $ go test main_test.go main.go -coverprofile=cover.out $ go tool cover -html=cover.out -o cover.html $ open cover.html このような形で確認できる。 だいたい8割を超えているので次に進む。 dockerを立ち上げてbuildを実行してみる。 先程のDockerfileの設定によって、testの実施も行う。 cmd.shに、stage2で実行したいコマンドを記述しよう。 cmd.sh #!/bin/sh -eux ./main < test.txt 記述できたら、コンテナーを起動する。 コマンドをいちいち打つのは面倒なので、Makefileでコマンドを単純化する。 Makefile NAME := gotail .PHONY: all all: docker-build docker-run .PHONY: docker-build docker-build: docker build -t $(NAME) . .PHONY: docker-run docker-run: docker run --rm $(NAME) $ make これで、一連の実行が確認できればOKです。 GitHub ActionsによるCI/CD(Go,Docker) GitHub ActionsではGitHub上でtestを走らせたり、buildをしたりできるので大変便利です。 今回はgo単体で検証するActionとDockerを起動するActionを試してみる。 まず、次のディレクトリ構成にしておく必要がある。 gotail └─.github └── workflows └── go.yml Actionの設定はYAMLファイルで記述する。 まずは、go.ymlを記述しよう。 go.yml on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Set up Go uses: actions/setup-go@v2 with: go-version: 1.15 - name: Build run: go build -v ./main.go - name: Test run: go test -v ./main_test.go main.go 対象のブランチ上へpushとpullrequestを行った際に、Actionが走る設定になっている。 また、buildとtestを実行できる。 次にdockerの起動も試してみる。 gotail └─.github └── workflows └── docker.yml docker.yml name: CI to Docker Hub on: push: branches: [ master ] pull_request: branches: [ master ] jobs: build: runs-on: ubuntu-latest steps: - name: Check Out Repo uses: actions/checkout@v2 - name: Login to Docker Hub uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_HUB_USERNAME }} password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v1 - name: Build and push id: docker_build uses: docker/build-push-action@v2 with: context: ./ file: ./Dockerfile push: true tags: ${{ secrets.DOCKER_HUB_USERNAME }}/simplewhale:latest - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} - name: Run run: make このsecretsはGithubやDockerhub上での設定が必要です。 まず、Docker Hubにアクセスします。 サインインができたら、次に右上にあるアイコンをクリックし、メニューを表示、以下の項目をクリックします。 次に、Security→New Access Tokenの順にクリック。 Tokenが表示されるので、適当にtitleなどを入力し、Copyしてウインドウを閉じます。 次にGithubにアクセスします。 Setting→Secret→New repository secretをクリックしてそれぞれ先程のTokenやDocker Hubのユーザー名を設定します。 以上で設定は完了です。 最後にリポジトリにpushかpullrequestを行うと自動でActionが実行されます。 今回のコード等は、この記事を執筆している段階では、まだmasterにマージはしていませんが、Githubにもアップしているので、参考になるかもしれません、リンク貼っておきます。 https://github.com/Iovesophy/gotail まとめ お疲れ様でした、ここまで読んでいただきありがとうございます! Go言語は初めてでしたが、Go言語のメリットとしてよく挙げられる、初心者でも理解しやすい、処理の速度が速い、少ないコード実装できる、ライブラリが豊富、並行処理が可能、安全性が高いというのはまさにその通りであると感じました。 次は簡単なアプリケーション制作や並行処理を試してみたいと思います。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む