1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

インフラエンジニアのためのGo言語入門

1
Posted at

はじめに

こんばんは、mirukyです。

インフラエンジニアの方でGoが好きな方は結構多い気がします。Shell Scriptはもちろんですが、Goもインフラ関連のツール開発でよく使われる言語ですよね。

今回は、指定したURLへのHTTPリクエストを通じて接続先の状態を確認し、異常があれば終了コードで知らせるツールをGoで作りながら、実践的に解説していきます。

目次

  1. 作るツールと最初のプログラム
  2. 値と条件で判定する
  3. スライスとmapで複数件を扱う
  4. 関数、構造体、ポインタ
  5. エラーとdeferで失敗と後処理を書く
  6. HTTP応答を確認する
  7. infra-checkを完成させる

1. 作るツールと最初のプログラム

1-1. infra-checkの入力と終了コード

今回作成するツール、命名:infra-checkはURLへ順にHTTP GETを送るツールです。200〜299の応答は正常、それ以外と通信エラーは異常とし、残りのURLも確認するものです。

# 7章でビルドした後に実行する、完成形の使い方です
./bin/infra-check -timeout=1s http://127.0.0.1:18080/fail http://127.0.0.1:18080/ok
# 直前のコマンドの終了コードを表示します
echo $?

補足ですが終了コードとは、プログラムが終了時にシェルへ返す整数です。異常を一度記録すると後の正常では取り消さず、全件を確認してから0か1を返します。

503の後に200を受けても異常の記録を保持し、終了コード1を返す仕組み

引数なしで./bin/infra-checkを実行すると、要求を送る前に2で終わります。HTTPの異常を示す1とは区別できる仕様です。

引数なしでは使用方法を表示し、HTTPを送る前に終了コード2を返す

言うまでもないかもですがシェルの&&は、左のコマンドが終わるまで待ち、終了コードが0のときだけ、右に書いた既存の./deploy.shを実行します。

シェルの&&は終了コード0なら後続を実行し、0以外なら実行しない

1-2. 最初のmainとgo run

Goは、参考リンクにある公式の導入手順でインストールします。

作業ディレクトリにモジュールを作ります。モジュールはコードを管理する単位で、go mod initが定義ファイルのgo.modを生成します。

# 作業ディレクトリを作り、モジュールを初期化します
mkdir infracheck && cd infracheck
go mod init example.com/infracheck
# 最初のプログラムを置くディレクトリです
mkdir -p steps/01-hello

最初のプログラムをsteps/01-hello/main.goに保存します。

package main

import "fmt"

func main() {
	// mainから実行が始まり、1行を出力します。
	fmt.Println("GoでHTTPを確認します")
}

package mainのfunc main()から実行が始まります。//から行末まではコメントです。

main.goの各行の役割と、Printlnが一行の文字列を出力する対応

go versionで、インストールしたGoのバージョンを確認します。続いてgo run ./steps/01-helloを実行し、保存したmain.goからプログラムをコンパイルして動かします。

# Goのバージョンを表示し、ソースをコンパイルしてその場で実行します
go version
go run ./steps/01-hello
go version go1.27.3 darwin/arm64
GoでHTTPを確認します

Go 1.27.3のバージョン表示と最初のプログラムを動かした保存済み実行記録

go build -o bin/helloなら実行ファイルが残り、./bin/helloで起動できます。プログラムが返した0・1・2をシェルで受け取るため、完成したコマンドラインツールもビルドしたファイルを直接実行します。

# 実行ファイルをbin/helloへ作り、直接実行します
go build -o bin/hello ./steps/01-hello
./bin/hello

go runはコンパイルして実行し、go buildはファイル作成で終了する。起動は別に./bin/helloを入力する

2. 値と条件で判定する

2-1. 変数の宣言と代入

以降の例も、go runに渡すパスのディレクトリへmain.goを置きます。最初はsteps/02-variables/main.goです。

package main

import "fmt"

func main() {
	// 宣言で名前を用意し、その後の代入で値を変更します。
	var port int = 8080
	host := "web-01"
	host = "web-02"
	port = 8081
	const readyStatus = 200
	fmt.Println(host, port, readyStatus)
}
# 宣言と代入を終えた後の値を表示します
go run ./steps/02-variables
web-02 8081 200

var port int = 8080は型を書いた宣言で、host := "web-01"は右辺の値から型を決める短い変数宣言です。host = "web-02"は同じhostへの代入です。

同じhostの名前と型を保ち、現在の値をweb-01からweb-02へ更新する

同じmainへhost := "web-03"を足すとビルドエラーです。:=の左辺には新しい変数が1つ以上必要ですが、hostは宣言済みです。

重複する短い変数宣言によるエラーと、宣言を戻した後の実行ログ

const readyStatus = 200で宣言した定数に、後から別の値は代入できません。readyStatus = 503を足すとビルドエラーになり、port = 8081のような変数への代入は通ります。

変数portには代入でき、定数readyStatusへ代入する行はビルド時に拒否される

2-2. 型で決まる値と演算

importを括弧で囲むと、複数のパッケージを取り込めます。

package main

import (
	"fmt"
	"time"
)

func main() {
	// 型に応じて扱える値と演算が決まります。
	name := "web-01"
	port := 8080
	enabled := true
	timeout := 1500 * time.Millisecond
	fmt.Printf("name=%s port=%d enabled=%t timeout=%s\n", name, port, enabled, timeout)
	fmt.Println(1+2, "1"+"2")
}
# 型ごとの表示と、演算の結果を確認します
go run ./steps/03-types
name=web-01 port=8080 enabled=true timeout=1.5s
3 12

nameは文字列のstring、portは整数のint、enabledは真偽値のboolです。fmt.Printfは、書式指定子と引数を順番に対応させて表示します。

Printfの四つの書式指定子と引数が順番に対応し、一行の表示になる

1+2は整数の足し算で、結果は3です。"1"+"2"は文字列の連結で、結果の"12"は1と2を並べた2文字です。port + "1"は、整数と文字列の演算なのでビルドエラーになります。

整数の足し算は3、文字列の連結は2文字の12となり、intと文字列の演算はビルド時に拒否される

time.Durationはナノ秒の整数で、1500 * time.Millisecondは1,500,000,000です。%sでは1.5sと表示されます。

time.Durationはナノ秒の整数を保持し、1500ミリ秒を文字列では1.5sと表示する

2-3. ifで2xxを判定する

statusが503のときの判定を出力します。

package main

import "fmt"

func main() {
	status := 503
	// 両方の比較がtrueのときだけ正常と判定します。
	if status >= 200 && status < 300 {
		fmt.Println("正常", status)
	} else {
		fmt.Println("異常", status)
	}
}
# 503に対する判定を表示します
go run ./steps/04-if
異常 503

比較式はtrueかfalseのboolを返します。&&は両辺がtrueのときだけtrueなので、この条件は2xxだけを表します。境界の200と299は正常、199と300は異常です。

正常とする200以上300未満の範囲と、境界の四つの値の判定

Goの&&は、左の比較がtrueのときだけ右の比較を評価します。503では503 >= 200がtrueなので、右の503 < 300まで評価してfalseになります。

503の条件式を左から評価し、trueとfalseから式全体のfalseを求める

条件式全体がfalseなので、if側の表示は実行しません。elseにあるfmt.Printlnが異常 503を表示し、mainの末尾へ進みます。

条件がfalseなのでelse側だけを実行し、異常503を表示してmainの末尾へ進む

3. スライスとmapで複数件を扱う

3-1. 一覧を順に処理し、キーで集計する

確認先のパスを一覧にして、順に表示します。

package main

import "fmt"

func main() {
	urls := []string{"/ok", "/fail"}
	urls = append(urls, "/slow")
	// rangeは位置と値を順に返し、位置は0から始まります。
	for index, address := range urls {
		fmt.Println(index, address)
	}
	fmt.Println("件数", len(urls))
}
# 一覧の位置と値、件数を表示します
go run ./steps/05-range
0 /ok
1 /fail
2 /slow
件数 3

[]string{"/ok", "/fail"}は、順序付きの一覧である文字列のスライスです。appendは要素を末尾へ足したスライスを返すので、urlsへ代入し直します。

スライスへ/slowを追加して受け直し、rangeが位置0から順に文字列を取り出す

rangeは0から始まる位置と値を先頭から返します。コマンドライン引数もos.Argsというスライスで、os.Args[0]はプログラム名です。完成版のmainは残りのos.Args[1:]をrunへ渡します。

os.Argsのプログラム名を除いた三要素が、フラグも含めてrun内のargsの位置0から始まる

ステータスごとの件数を数える

位置が不要なrangeでは、for _, statusのように位置を_で捨てます。

package main

import "fmt"

func main() {
	counts := map[int]int{}
	// 同じステータスをキーにして件数を増やします。
	for _, status := range []int{200, 503, 200} {
		counts[status]++
	}
	// map自体の反復順に頼らず、表示するキーを指定します。
	fmt.Println("200の件数", counts[200])
	fmt.Println("503の件数", counts[503])
	count, exists := counts[404]
	fmt.Println("404の値と存在", count, exists)
}
# キーごとの件数と、存在しないキーの結果を表示します
go run ./steps/06-map
200の件数 2
503の件数 1
404の値と存在 0 false

map[int]int{}は、整数をキーと値に使う空のmapです。counts[status]++は、未登録のキーなら0から数え始めます。

200、503、200の順に件数を増やし、最後の200だけが1から2へ変わる

count, exists := counts[404]のように二つの値で受け取れば、未登録をexists == falseで判定できます。値の取得ではキーは追加されません。

未登録のキー404を参照するとcountは0、existsはfalseとなり、mapの要素は増えない

mapをrangeで反復する順序は仕様で決まっておらず、実行ごとに変わることがあります。この例はキーを指定して表示するので、出力順は毎回同じです。

4. 関数、構造体、ポインタ

4-1. 関数と構造体で結果を返す

2章の条件式を関数にします。mainの外にfuncで定義し、mainから名前で呼び出します。

package main

import "fmt"

func isHealthy(status int) bool {
	// 引数を受け取り、判定した値を呼び出し元へ返します。
	return status >= 200 && status < 300
}

func main() {
	fmt.Println(isHealthy(200))
	fmt.Println(isHealthy(503))
}
# 200と503を関数へ渡した結果を表示します
go run ./steps/07-functions
true
false

isHealthyはintを受け取りboolを返します。isHealthy(503)では503がstatusに入り、returnの結果falseが呼び出し元へ戻ります。

isHealthyのstatusへ503が入り、戻ったfalseを同じ呼び出しのPrintlnが表示する

1件の結果を構造体にする

URLとステータスを、1つの値にまとめます。

package main

import "fmt"

type Result struct {
	URL    string
	Status int
}

func main() {
	// 1件の確認結果を、名前付きのフィールドで保持します。
	result := Result{URL: "/ok", Status: 200}
	fmt.Println(result.URL, result.Status)
}
# 構造体のフィールドを表示します
go run ./steps/08-struct
/ok 200

type Result structは、名前付きフィールドを持つ型を定義します。Result{URL: "/ok", Status: 200}で値を作り、.でフィールドを取り出します。

Result型の変数resultにあるURLとStatusを、型・値・フィールド参照の式へ対応させる

Result{}はフィールドがゼロ値で、URLは空文字列、Statusは0です。6章のcheckOneは、通信に失敗するとこれを返します。

Resultのフィールドを省略すると、URLは空文字列、Statusは0という型ごとのゼロ値になる

4-2. &と*で元の値を変更する

コピーとポインタで、元の変数への影響を比べます。

package main

import "fmt"

func main() {
	port := 8080
	copyPort := port
	copyPort = 8443
	fmt.Println("コピーを変更", port, copyPort)
	// &で変数の場所を取得し、*でその場所の値を変更します。
	pointer := &port
	*pointer = 8443
	fmt.Println("元の値を変更", port)
}
# コピーを変えた場合と、ポインタ経由で変えた場合を表示します
go run ./steps/09-pointer
コピーを変更 8080 8443
元の値を変更 8443

copyPort := portは8080のコピーなので、copyPortを変えてもportは8080のままです。関数の引数にもコピーが渡ります。

copyPortへ値をコピーしてから8443を代入しても、元のportは8080を保つ

&portでportの場所を取得し、pointer := &portでpointerに保持します。pointerの型は*intで、その場所にある元のportを参照します。

pointerの型は*intで、整数8080を持つportの場所を参照する

*pointer = 8443は、pointerが指している変数へ代入します。参照先がportなので、同じportの値が8080から8443へ変わります。

*pointerへ8443を代入すると、同じ参照先のportが8080から8443へ変わる

7章では、options.Durationが返す*time.Durationから*timeoutで期限を取り出します。*http.Clientと*http.Responseもポインタ型です。

5. エラーとdeferで失敗と後処理を書く

5-1. 失敗を返し、後処理を予約する

文字列をポート番号へ変換し、範囲も確かめます。

package main

import (
	"fmt"
	"strconv"
)

func parsePort(text string) (int, error) {
	port, err := strconv.Atoi(text)
	if err != nil {
		return 0, err
	}
	if port < 1 || port > 65535 {
		return 0, fmt.Errorf("portは1〜65535です")
	}
	return port, nil
}

func main() {
	// 値を使う前に、もう一つの戻り値で成否を確認します。
	for _, text := range []string{"8080", "zero", "0"} {
		port, err := parsePort(text)
		if err != nil {
			fmt.Println("失敗", text, err)
		} else {
			fmt.Println("成功", port)
		}
	}
}
# 3つの文字列を変換した結果を表示します
go run ./steps/10-errors
成功 8080
失敗 zero strconv.Atoi: parsing "zero": invalid syntax
失敗 0 portは1〜65535です

parsePortはintと、失敗の理由を持つerrorを返します。成功時のerrorは、値がないことを示すnilです。||はどちらかがtrueならtrueです。

文字列8080、zero、0で、整数への変換と範囲確認の結果、戻るintとerrorを比較する

呼び出し側は値を使う前にif err != nilで成否を確かめます。失敗時の0をポート番号に使わないためです。

関数から戻るときに後処理する

deferの実行順を出力します。

package main

import "fmt"

func work() {
	// この関数から戻るときに実行する処理を予約します。
	defer fmt.Println("後処理")
	fmt.Println("本処理")
}

func main() {
	work()
	fmt.Println("呼び出し元へ戻る")
}
# deferを含む関数の出力順を表示します
go run ./steps/11-defer
本処理
後処理
呼び出し元へ戻る

deferで指定した呼び出しは、その関数から戻る直前に実行されます。workではdeferの行が先でも、後処理の出力は戻る直前になり、その後にmainが続きます。

deferで予約した出力はworkから戻る直前に実行され、続いてmainが表示する

returnでも関数の末尾に達しても、deferの処理は実行されます。os.Exitはプロセスをその場で終え、deferの処理を飛ばします。そのため完成版は、os.Exitをmainの1か所だけで呼びます。

main以外の関数のreturnはdeferを実行して呼び出し元へ戻り、os.Exitは未実行deferを残してプロセスを終了する

6. HTTP応答を確認する

6-1. 学習用サーバーを起動する

HTTPの結果を自分で変えられるように、学習用サーバーを用意します。次のコードをfixture/main.goとして保存してください。

三つのパスが返すHTTPステータスと本文、追加した待機の対応

fixture/main.goの全文
package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"
)

func ok(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, "ok")
}

func fail(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusServiceUnavailable)
	fmt.Fprintln(w, "maintenance")
}

func slow(w http.ResponseWriter, r *http.Request) {
	time.Sleep(2 * time.Second)
	fmt.Fprintln(w, "slow")
}

func main() {
	addr := flag.String("addr", "127.0.0.1:18080", "学習用サーバーの接続先")
	flag.Parse()
	mux := http.NewServeMux()
	mux.HandleFunc("/ok", ok)
	mux.HandleFunc("/fail", fail)
	mux.HandleFunc("/slow", slow)
	// 学習用のHTTP応答をローカルの接続先だけに提供します。
	server := &http.Server{Addr: *addr, Handler: mux, ReadHeaderTimeout: time.Second}
	fmt.Println("学習用サーバー", *addr)
	if err := server.ListenAndServe(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

サーバーがHTTP要求を待ち受けている間は、端末Bのプロンプトへ戻りません。同じパソコンで端末Aを開き、サーバーを動かしたまま要求を送ります。

# 端末Bでinfracheckディレクトリへ移動してから、学習用サーバーを起動します。Ctrl+Cで停止します
go run ./fixture

同じパソコンのクライアントが、ループバックアドレスで待つ学習用HTTPサーバーへ要求を送る

サーバーを起動したまま、端末Aのcurlでステータスコードだけを表示します。

# 端末Aで、ローカル通信をプロキシへ送らずにステータスコードを表示します
curl --noproxy '*' --silent --output /dev/null --write-out '%{http_code}\n' http://127.0.0.1:18080/ok
curl --noproxy '*' --silent --output /dev/null --write-out '%{http_code}\n' http://127.0.0.1:18080/fail

次はGoから同じURLへ要求を送り、応答の異常と通信エラーを判定します。

ローカルのokとfailへcurlを実行し、HTTP200と503を取得したログ

6-2. 通信エラーとステータスコードを判定する

infra-check/main.goから、1件を確認するcheckOneを抜粋します。全文は7章にあります。

func checkOne(client *http.Client, address string) (Result, error) {
	response, err := client.Get(address)
	if err != nil {
		return Result{}, err
	}
	// 応答を受け取った場合は、呼び出し元へ戻る前にBodyを閉じます。
	defer response.Body.Close()
	return Result{URL: address, Status: response.StatusCode}, nil
}

client.Getは(*http.Response, error)を返し、既定ではリダイレクトに従います。503が届いても通信は成功なので、errはnil、response.StatusCodeは503です。runがisHealthyで2xxか判定します。

HTTP200と503ではerrがnilで、期限切れではerrが返る。runはステータスかエラーから判定する

期限内に応答がない場合や、待ち受けのないポートへ接続した場合は、errがnil以外になります。このときはゼロ値のResult{}とエラーを返します。

エラーを先に確認し、errがnilの経路だけでBody.Closeを予約する

response.Bodyは呼び出し側が閉じます。defer response.Body.Close()をcheckOneへ置くと、1件を処理して戻る際にCloseを呼べます。Getとdeferをrunのループ内へ置く実装では、Closeの呼び出しはrunから戻る直前まで遅れます。

checkOne内のdeferは1件ごとにCloseを呼び、run内で予約する比較例はrunから戻る直前まで待つ

7. infra-checkを完成させる

7-1. flagで期限と指定方法の誤りを扱う

完成したinfra-check/main.goの全文です。

package main

import (
	"flag"
	"fmt"
	"net/http"
	"os"
	"time"
)

type Result struct {
	URL    string
	Status int
}

func isHealthy(status int) bool {
	return status >= 200 && status < 300
}

func checkOne(client *http.Client, address string) (Result, error) {
	response, err := client.Get(address)
	if err != nil {
		return Result{}, err
	}
	// 応答を受け取った場合は、呼び出し元へ戻る前にBodyを閉じます。
	defer response.Body.Close()
	return Result{URL: address, Status: response.StatusCode}, nil
}

func run(args []string) int {
	options := flag.NewFlagSet("infra-check", flag.ContinueOnError)
	timeout := options.Duration("timeout", time.Second, "1接続先の期限")
	if err := options.Parse(args); err != nil {
		if err == flag.ErrHelp {
			return 0
		}
		return 2
	}
	if options.NArg() == 0 || *timeout <= 0 {
		fmt.Fprintln(os.Stderr, "使用方法: infra-check [-timeout=1s] URL...")
		return 2
	}
	client := &http.Client{Timeout: *timeout}
	failed := false
	for _, address := range options.Args() {
		result, err := checkOne(client, address)
		if err != nil {
			fmt.Fprintln(os.Stderr, "異常", address, err)
			failed = true
		} else if !isHealthy(result.Status) {
			fmt.Fprintln(os.Stderr, "異常", result.URL, result.Status)
			failed = true
		} else {
			fmt.Println("正常", result.URL, result.Status)
		}
	}
	if failed {
		return 1
	}
	return 0
}

func main() {
	// runとcheckOneの後処理が済んでから終了コードを返します。
	os.Exit(run(os.Args[1:]))
}

flag.NewFlagSetで、このツールのフラグを定義するoptionsを作ります。options.Durationは-timeoutを登録し、既定値1秒の*time.Durationを返します。

timeoutの参照先を1sから3sへ更新し、time.Durationの値をClient.Timeoutへコピーする

options.Parse(args)はフラグでない最初の引数で解析を止め、残りはoptions.Args()、その個数はoptions.NArg()が返します。そのため-timeoutはURLより前に書きます。URL -timeout=3sの順では期限が1秒のままで、-timeout=3sは2件目の確認先として異常になります。

標準flagは最初のURLで解析を止めるため、後ろの-timeout=3sは未解析の引数として残る

ifは、条件の前に;で区切った文を置けます。ContinueOnErrorなので、未知のフラグや-timeout=oopsではParseがエラーを返し、runは2で戻ります。URLの未指定と0以下の期限も、送信前に2を返します。-hは使い方を表示し、flag.ErrHelpを受けて0で終わります。

# 実行ファイルを作り、指定方法の誤りと-hの終了コードを1つずつ表示します
go build -o bin/infra-check ./infra-check
./bin/infra-check; echo $?
./bin/infra-check -timeout=0 http://127.0.0.1:18080/ok; echo $?
./bin/infra-check -timeout=oops http://127.0.0.1:18080/ok; echo $?
./bin/infra-check -h; echo $?

URL未指定と不正な期限は終了2、ヘルプは終了0となる実行ログ

7-2. runの戻り値をos.Exitへ渡す

else ifは、前のifが成り立たないときだけ評価されます。!はboolを反転し、異常が1件でもあればfailedがtrueになります。

正常の行は標準出力、異常の行は標準エラーへ出力します。

os.Exit(run(os.Args[1:]))では、引数のrunが先に最後まで実行されます。その時点で各Body.Closeは済み、os.Exitが0、1、2のいずれかで終了します。

fixtureを端末Bで起動したまま、端末Aで全件正常の場合と503を含む場合を確認します。

# 全件正常の場合と、503を含む場合の終了コードを表示します
./bin/infra-check http://127.0.0.1:18080/ok; echo $?
./bin/infra-check http://127.0.0.1:18080/ok http://127.0.0.1:18080/fail; echo $?

1回目は正常の1行を出して0、2回目は/failの異常の行が加わって1です。

正常なURLでは終了0、HTTP503を含むと終了1となる実行ログ

/slowはサーバー側で2秒待ってから応答します。-timeout=1sでは応答前に期限が切れて通信エラーとなり終了コード1、-timeout=3sではHTTP200を受け取って終了コード0です。

# 同じ/slowを、期限1秒と3秒で確認します
./bin/infra-check -timeout=1s http://127.0.0.1:18080/slow; echo $?
./bin/infra-check -timeout=3s http://127.0.0.1:18080/slow; echo $?

同じslowへの要求が期限1秒で失敗し、3秒で成功する実行ログ

端末BのfixtureをCtrl+Cで止めると待ち受けがなくなり、/okも通信エラーで1になります。再起動すれば0に戻ります。

# 端末Bでfixtureを止めた後と、再起動した後に同じコマンドを実行します
./bin/infra-check http://127.0.0.1:18080/ok; echo $?

学習用サーバーの停止で接続を拒否され、再起動後に成功する実行ログ

7-3. isHealthyの境界値をテストする

テストはinfra-check/main_test.goに置きます。go testは、_test.goで終わるファイルのfunc TestXxx(t *testing.T)形式の関数を実行します。TestIsHealthyを抜粋します。

func TestIsHealthy(t *testing.T) {
	// 成功範囲の両端と、その直前・直後を確認します。
	cases := []struct {
		status int
		want   bool
	}{{199, false}, {200, true}, {299, true}, {300, false}, {503, false}}
	for _, test := range cases {
		if got := isHealthy(test.status); got != test.want {
			t.Errorf("status=%d got=%t want=%t", test.status, got, test.want)
		}
	}
}

casesは入力のstatusと期待値のwantを組にした、型名のない構造体のスライスです。gotがwantと違うとt.Errorfが失敗を記録し、次の組へ進みます。

上限を誤って300以下にすると、300だけでwantとgotが食い違い、503まで検査して全体は失敗になる

上限をstatus <= 300に変えると、300の組でstatus=300 got=true want=falseが記録されます。status < 300に戻せば全組が一致します。

# 詳細表示でテストを実行します。条件を書き換えた後と戻した後で比べます
go test -v ./infra-check
# モジュール全体の静的検査とテストです
go vet ./...
go test ./...

300の境界条件で失敗したテストと、条件を戻して成功したテストのログ抜粋

TestRunはhttptest.NewServerでローカルサーバーを起動し、9通りの引数でrunの戻り値を比べます。最後はサーバーを閉じ、通信エラーでも1になるか確認します。

TestRunからrunを直接呼び、200・503・URLなしの代表例で戻り値を期待値と比較する

infra-check/main_test.goの全文
package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
	"time"
)

func TestIsHealthy(t *testing.T) {
	// 成功範囲の両端と、その直前・直後を確認します。
	cases := []struct {
		status int
		want   bool
	}{{199, false}, {200, true}, {299, true}, {300, false}, {503, false}}
	for _, test := range cases {
		if got := isHealthy(test.status); got != test.want {
			t.Errorf("status=%d got=%t want=%t", test.status, got, test.want)
		}
	}
}

func testEndpoint(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path == "/fail" {
		w.WriteHeader(http.StatusServiceUnavailable)
	} else if r.URL.Path == "/slow" {
		time.Sleep(80 * time.Millisecond)
	}
}

func TestRun(t *testing.T) {
	server := httptest.NewServer(http.HandlerFunc(testEndpoint))
	defer server.Close()
	cases := []struct {
		name string
		args []string
		want int
	}{
		{"healthy", []string{server.URL + "/ok"}, 0},
		{"status failure", []string{server.URL + "/fail"}, 1},
		{"mixed results", []string{server.URL + "/fail", server.URL + "/ok"}, 1},
		{"timeout", []string{"-timeout=10ms", server.URL + "/slow"}, 1},
		{"no URL", nil, 2},
		{"bad flag", []string{"-unknown"}, 2},
		{"bad duration", []string{"-timeout=oops", server.URL}, 2},
		{"zero timeout", []string{"-timeout=0", server.URL}, 2},
		{"help", []string{"-h"}, 0},
	}
	for _, test := range cases {
		if got := run(test.args); got != test.want {
			t.Errorf("%s: got=%d want=%d", test.name, got, test.want)
		}
	}
	// 終了した接続先への通信エラーは、HTTP503と同じ終了1へ集約します。
	server.Close()
	if got := run([]string{server.URL}); got != 1 {
		t.Errorf("closed endpoint: got=%d want=1", got)
	}
}

おわりに

ここまでお付き合いいただきありがとうございます。

infra-checkは、応答の状態と通信エラーを終了コードへまとめるツールです。接続先のURLや期限を変えて、判定がどう変わるか試してみてください。

ではまた、お会いしましょう。

参考リンク

作るツールと最初のプログラム

Goの導入と言語仕様

標準ライブラリ

シェルの条件付き実行

HTTPの確認

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?