- 投稿日:2020-08-11T23:37:38+09:00
【Go】【Ginkgo】FizzBuzzをGinkgoでテストしてみる
はじめに
最近Goの勉強をしてて、簡単なWebサーバーを作ってみたり、コマンドラインで動作するツールを作ったり、、、としているんですけど、そういやテスト書いたことないなあと思って今回の記事を書くことにしました!
Ginkgoを選んだ理由
Ginkgoの書き方がなんとなくrspecに似てるからです。
昔Railsを書いていたことがあるので、簡単に書けるんじゃないかな〜っていう安易な発想です。テストの準備
Ginkgoをインストールします。
go get github.com/onsi/ginkgo/ginkgo go get github.com/onsi/gomega/...ディレクトリとテスト用ファイルを作成します。
mkdir fizzbuzz cd fizzbuzz ginkgo bootstrap ginkgo generate fizzbuzz実装
今回作成するテストケースは以下の通りとします。よく見るFizzBuzzですね。
- 数値が3の倍数であれば「Fizz」を返す
- 数値が5の倍数であれば「Buzz」を返す
- 数値が3の倍数かつ5の倍数であれば「FizzBuzz」を返す
- 数値が3の倍数でも5の倍数でもなければ、そのまま文字列に変換して返す
早速ですが実装は以下の通りです。
シンプルなFizzBuzzなので特に何もないですね。fizzbuzz.gopackage fizzbuzz import ( "strconv" ) func Convert(i int) (string, error) { if i%15 == 0 { return "FizzBuzz", nil } if i%3 == 0 { return "Fizz", nil } if i%5 == 0 { return "Buzz", nil } return strconv.Itoa(i), nil }こちらがテストコードになります。
こういう書き方がいいのかダメなのかもわかってないですけど、それっぽく書いてみました。fizzbuzz_test.gopackage fizzbuzz import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "strconv" ) var _ = Describe("fizzbuzz", func() { Describe("Convertメソッド", func() { Context("引数が3の倍数でも5の倍数でもない場合", func() { data := []int{1, 4, 8, 11, 13, 14, 17, 19, 23, 26} for idx := range data { // rangeの第2引数はポインタなのでそのままアサーションに使用すると正しくテストが実行されない v := data[idx] r, err := Convert(v) It("そのまま文字列変換した値を返すこと", func() { Expect(r).To(Equal(strconv.Itoa(v))) }) It("エラーがnilであること", func() { Expect(err).To(BeNil()) }) } }) Context("引数が3の倍数", func() { data := []int{3, 6, 9, 12, 18, 21, 24, 27, 33, 36} for idx := range data { // rangeの第2引数はポインタなのでそのままアサーションに使用すると正しくテストが実行されない v := data[idx] r, err := Convert(v) It("Fizzという文字列を返すこと", func() { Expect(r).To(Equal("Fizz")) }) It("エラーがnilであること", func() { Expect(err).To(BeNil()) }) } }) Context("引数が5の倍数", func() { data := []int{5, 10, 20, 25, 35, 40, 50, 55, 65, 70} for idx := range data { // rangeの第2引数はポインタなのでそのままアサーションに使用すると正しくテストが実行されない v := data[idx] r, err := Convert(v) It("Buzzという文字列を返すこと", func() { Expect(r).To(Equal("Buzz")) }) It("エラーがnilであること", func() { Expect(err).To(BeNil()) }) } }) Context("引数が3の倍数かつ5の倍数", func() { data := []int{15, 30, 45, 60, 75, 90, 105, 120, 135, 150} for idx := range data { // rangeの第2引数はポインタなのでそのままアサーションに使用すると正しくテストが実行されない v := data[idx] r, err := Convert(v) It("FizzBuzzという文字列を返すこと", func() { Expect(r).To(Equal("FizzBuzz")) }) It("エラーがnilであること", func() { Expect(err).To(BeNil()) }) } }) }) })おわりに
今回は、Goで簡単なテストを書いてみました。
標準のテストを書いたことがない状態で、いきなりGinkgoを書いていいものか悩みましたが、まあよしとしましょう。
何かの参考になれば幸いです。
- 投稿日:2020-08-11T22:00:04+09:00
nuxtのformからPOSTされたデータをgoのサーバで受け取る(私的メモ)
nuxt.js(vueファイル)
Vuetifyのコンポーネントを活用し、formを作成、validationとresetボタンを実装しました。
<template> <v-form ref="form" v-model="valid"> <v-text-field v-model="title" :counter="10" :rules="titleRules" label="Title" required ></v-text-field> <v-textarea v-model="description" :counter="100" :rules="descriptionRules" label="Description" required ></v-textarea> <v-textarea v-model="content" :counter="100" :rules="contentRules" label="Content" required ></v-textarea> <v-btn :disabled="!valid" color="success" class="mr-4" @click="submit"> SUBMIT </v-btn> <v-btn color="error" class="mr-4" @click="reset"> Reset Form </v-btn> </v-form> </template> <script> export default { data: () => ({ valid: false, title: '', titleRules: [ (v) => !!v || 'Title is required', (v) => (v && v.length <= 20) || 'Title must be less than 20 characters', ], description: '', descriptionRules: [ (v) => !!v || 'Description is required', (v) => (v && v.length <= 100) || 'Description must be less than 100 characters', ], content: '', contentRules: [ (v) => !!v || 'Content is required', (v) => (v && v.length <= 100) || 'Content must be less than 100 characters', ], }), methods: { post() { this.$axios.$post('http://localhost:10000/articles', { title: this.title, desc: this.description, content: this.content, }) }, submit() { this.$refs.form.validate() if (this.valid) { this.post() alert('「' + this.title + '」を登録しました。') document.location = '/articles' } }, reset() { this.$refs.form.reset() }, }, } </script>validationに合格するとsubmitボタンの色が変化し、ボタンをクリックするとイベントが発火しsubmit()が呼ばれます
その中で、post()メソッドを呼び出し$axiosモジュールを使ってAPIサーバにPOSTメソッドをデータ付きで送信します。axiosはデフォルトでContent-Typeをapplication/jsonで通信します。go
# CORS対策 func withCORS(fn http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") fn(w, r) } } # muxにハンドラを登録している部分です(gorilla/muxを使用) myRouter.HandleFunc("/articles", withVars(withDB(db, withCORS(createNewArticle)))).Methods("POST") # 登録してるハンドラ定義部分です func createNewArticle(w http.ResponseWriter, r *http.Request) { log.Println("called createNewArticle") # db接続 d := GetVar(r, "db").(Database) db := d.init() # JsonをArticle構造体としてパース article := ParseJsonArticle(w, r) # 検証と保存 if valid := article.validate(); valid { db.Create(&article) if db.NewRecord(article) { log.Println("新規articleの保存に失敗しました。") } } }フォームから適当なデータを送ってみると....
(一部) Access to XMLHttpRequest at 'http://localhost:10000/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. xhr.js:178 POST http://localhost:10000/articles net::ERR_FAILEDCORSポリシーでクライアントからサーバーへのリクエストがブロックされていると怒られています。
特にこの部分、
Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.'Access-Control-Allow-Origin' ヘッダーはgoでハンドラの直前にチェインさせている関数(withCORS)で確かに付与しているハズ...
(。´・ω・)ん?
go側のログを見る限り、ハンドラ関数に到達すらしていない模様。
こんな記事がありました。
https://qiita.com/att55/items/2154a8aad8bf1409db2bプリフライトリクエストとシンプルリクエストなんてものがあるようです。
Content-Type=''application/json"の場合、クロスオリジン(ドメインかポートがサーバとクライアントで違う)の場合、一旦OPTIONSメソッドが走り、安全確認できたら本メソッドが走るイメージかなと理解します。
https://github.com/gorilla/mux#user-content-handling-cors-requests
に沿って、サーバ側のコードを直します。func articlesCORSHandling(w http.ResponseWriter, r *http.Request){ # レスポンスヘッダーの追加 w.Header().Set("Access-Control-Allow-Origin","http://localhost:3000") w.Header().Set( "Access-Control-Allow-Headers", "Origin, ContentType,") w.Header().Set( "Access-Control-Allow-Methods","GET, POST, OPTIONS" ) # GETメソッド if r.Method == http.MethodGet { log.Println("called GET /articles") d := GetVar(r, "db").(Database) db := d.init() var articles Articles db.Find(&articles) json.NewEncoder(w).Encode(articles) } # POST メソッド if r.Method == http.MethodPost { log.Println("called POST /articles") d := GetVar(r, "db").(Database) db := d.init() article := ParseJsonArticle(w, r) log.Println(article) if valid := article.validate(); valid { // log.Println("valid!!") db.Create(&article) if db.NewRecord(article) == false { log.Println("新規articleを保存しました") }else { log.Println("新規articleの保存に失敗しました。") } } } } func handleRequests() { r := mux.NewRouter() r.HandleFunc("/articles", withVars(withDB(db, articlesCORSHandling))).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) r.Use(mux.CORSMethodMiddleware(r)) log.Fatal(http.ListenAndServe(":10000", r)) }withCORSを関数内部に統合させる形でハンドラを定義し登録してます。
再度、適当なデータを送信してみると...2020/08/11 12:44:48 called POST /articles 2020/08/11 12:44:48 {{0 0001-01-01 00:00:00 +0000 UTC 0001-01-01 00:00:00 +0000 UTC <nil>} test test test} 2020/08/11 12:44:48 新規articleを保存しました成功しました。
- 投稿日:2020-08-11T21:15:46+09:00
Go構造体の`json:hoge`の正体を探る
TL;DR
type Person struct { Name string `json:"name"` }
`json:"name"`
は構造体のタグreflect.TypeOf(Person).Field(0).Get("json")
で取り出せる!はじめに
Go言語でjsonを扱うとき、構造体定義にjsonのフィールド名を書くと思います。
type Person struct { Name string `json:"name"` Age int `json:"age"` Address string `json:"address"` }おまじないと思って深く考えず使っていましたが、
- そもそも
`json:"name"`
って何?- ただのコメント?
- jsonのためだけの特別構文?
という疑問がわいたので調べてみました。
json:hoge は構造体のタグ
公式ドキュメントに早速答えがありました。
ちゃんとふだんから読みなさいThe Go Programming Language Specification - The Go Programming Language
A field declaration may be followed by an optional string literal tag, which becomes an attribute for all the fields in the corresponding field declaration. An empty tag string is equivalent to an absent tag. The tags are made visible through a reflection interface and take part in type identity for structs but are otherwise ignored.
フィールド宣言の後にはオプションで文字列リテラルのタグを付けることが可能で、これはフィールド宣言に対応する全てのフィールドの属性となります。空文字列のタグはタグ無しの場合と同等です。タグはリフレクションインターフェースにより可視化され、構造体の型の一意性1に関与しますが、それ以外の場合は無視されます。
(日本語は拙訳)
`json:"hoge"`
は構造体のタグであり、もともとGo言語にある構文を使っている(json用の特別なものではない)ことが分かります。タグの取り出し方
ドキュメントの通り、タグは
reflect
を使って取り出すことができます。import ( "fmt" "reflect" ) type Person struct { Name string `my name` Age int `Don't look at this!` } func main() { person := Person{Name: "Taro", Age: 23} // リフレクションで型情報取得 personType := reflect.TypeOf(person) for i := 0; i < personType.NumField(); i++ { // タグを取得 tag := personType.Field(i).Tag fmt.Println(tag) } }resultmy name Don't look at this!タグ内のキー
これだけでも便利ですが、タグを
key:"value"
の形式で書くことで複数の属性を持たせることができます。キーに対応する値はTag.Get(key)
で取り出せます。
encoding/json
はタグ中のjson
キーを参照していたのですね。import ( "fmt" "reflect" ) type Person struct { Name string `json:"name" ` Age int `json:"age"` // 他のキーも定義 Address string `json:"address" description:"where he lives" country:"Japan"` } func main() { person := Person{Name: "Taro", Age: 23, Address: "Tokyo"} personType := reflect.TypeOf(person) fmt.Println(personType.Field(0).Tag.Get("json")) // name fmt.Println(personType.Field(1).Tag.Get("json")) // age fmt.Println(personType.Field(2).Tag.Get("json")) // address // 他のキーも参照可能! fmt.Println(personType.Field(2).Tag.Get("description")) // where he lives fmt.Println(personType.Field(2).Tag.Get("country")) // Japan }実際に
json.Marshal
を読んでみると、(呼び出しを追うのが大変ですが)Tag.Get("json")
でフィールド名を取り出す処理が確認できます。タグを利用するライブラリ
json以外にもxml, yaml等色々なパッケージで利用されているみたいです。上手く活用していきたいですね!
- 投稿日:2020-08-11T19:06:41+09:00
Golang + Sonarqube + Codebuild
https://qiita.com/fake-deli-ca/items/e01a42a9d04cf0bab8c4
の後にJenkinsじゃなくてCodeBuildも対応してみたんだけど、異様にわかりにくくて、時間かかった…。結論
最初に結論から。
関係する箇所のみ抜粋してある。buildspec.ymlversion: 0.2 env: variables: GO_VERSION : "" PROJECT_VERSION : "" # 本来はParameter-Storeにすべき SONARQUBE_TOKEN : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" phases: install: runtime-versions: golang: ${GO_VERSION} commands: - type go && go version - chmod +x ./gradlew - export REGOPATH=`echo $GOPATH | cut -c 5-` - export PATH=${GOPATH}/bin:${PATH} && echo ${PATH} pre_build: commands: - echo 'setup' - make setup build: commands: - echo 'make & test' - make - make test - ./gradlew sonarqube -Dsonar.projectBaseDir=${REGOPATH}/src -Dsonar.login=${SONARQUBE_TOKEN} -Dsonar.projectVersion=${PROJECT_VERSION} --info下記のMakefileはBuildの記載がないので、このままでは動作しない。
.PHONY: all all: build .PHONY: setup setup: go get -u github.com/ory/go-acc .PHONY: test test: go test ./... -json > reports/test.json go-acc ./... -o reports/coverage.outbuild.gradleplugins { id "org.sonarqube" version "3.0" } sonarqube { properties { //FIXME property "sonar.host.url","https://sonarqube.foo.jp" property "sonar.projectKey", "hogehoge" property "sonar.projectName", "hogehoge Project" property "sonar.go.coverage.reportPaths", "(gitのpass)/reports/coverage.out" property "sonar.go.tests.reportPaths", "(gitのpass)/reports/test.json" //FIXME property "sonar.sources", "." property "sonar.exclusions", "**/*_test.go" property "sonar.tests", "." property "sonar.test.inclusions", "**/*_test.go" property "sonar.test.exclusions", "**/vendor/**" property "sonar.sourceEncoding", "UTF-8" } }確認すべき点
is not included in the project, ignoring coverage
Sensor Go Cover sensor for Go coverage [go] Load coverage report from '/codebuild/output/src532592822/src/(gitのpass)/coverage.txt' File '/codebuild/output/src532592822/src/(gitのpass)/AAA/BBB.go' is not included in the project, ignoring coverageとにかく、
is not included in the project, ignoring coverage
にやられた。どう設定していいのか全く分からない。
https://github.com/SonarSource/sonar-go/issues/218
を読むとGOPATHの設定が悪い、と書いてあるが、どうすれば治るのかは書いてない。
酷い…。結論を言うと、
sonar.projectBaseDir
の設定が悪い。ところが、皆様ご存じの通り、Codebuildは/codebuild/output/srcXXXXXXXXXX/src
の部分が毎回異なるため、何らかの利用できる変数で設定しなければならない。
普通に考えるとsonar.projectBaseDir=$GOPATH/src
とすればよいのだが、ところがである。Codebuildの$GOPATH
は/go:/codebuild/output/src532592822
となっており、/go: の部分が心底余計なのである。
上記のように設定すると、* What went wrong: Execution failed for task ':sonarqube'. > The folder '/go:/codebuild/output/src179970974/src' does not exist for 'xxxxx' (base directory = /codebuild/output/src179970974/src/(gitのpass))みたいなエラーになるのである。まあ当然だが。
というわけで強引にcutで削除して再設定した。本当にこれでいいのかどうかは知らない。
また、本来はbuild.gradleに全部書きたかったのだが、当然環境変数と展開できないので、-Dsonar.projectBaseDir=${REGOPATH}/src
という設定とした。reportPathsの変更
上記projectBaseDirの変更に伴う対応である。
普通、この辺のPathは$CODEBUILD_SRC_DIR
の直下に置かれるので、gitのpassは本来不要なのであるが、projectBaseDirを変更した関係上、ここも変更が必要である。property "sonar.go.coverage.reportPaths", "(gitのpass)/reports/coverage.out" property "sonar.go.tests.reportPaths", "(gitのpass)/reports/test.json"Gradleの利用
sonarqubeだけgradle使っててなんかおかしな感じではあるのだが、最初こんな感じでやってたらunzipのインストールが必要だったり、sonar-scannerのインストール位置が気持ち悪かったり、なんだか訳がわからなくなりそうだったので、あきらめて素直にgradle使うことにした。別に使わなくてもいいとは思う。ただ↓の設定でうまくいくところまでは試してない。少なくともGOPATHはこのままでは駄目だと思う。
また、gogradleで全部書き直すことも考えたが、なんかそこまでして綺麗にする理由もなかったのでやめ。buildspec.ymlphases: install: runtime-versions: golang: ${GO_VERSION} commands: - type go && go version # sonar-scanner install - yum -y install unzip - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip - unzip ./sonar-scanner-cli-4.4.0.2170-linux.zip pre_build: commands: - echo 'setup' - export PATH=${GOPATH}/bin:./sonar-scanner-4.4.0.2170-linux/bin:${PATH} && echo ${PATH} - make setup build: commands: - echo 'make & test' - make - make test - sonar-scanner -X -Dsonar.login=${SONARQUBE_TOKEN}go-accの利用
ここまで実施してみて、どうもCoverage率が異様に低いことに気が付く。
これも結論だけをいうと、go test -cover
のデフォルト挙動に問題があるということである。なので、go-acc
を利用することとした。The reason for this was that go test -cover is per default only recording code coverage for the package that is currently tested, not for all the packages from the project. If you have a lot of integration tests, your code coverage might be higher than you think!
https://www.ory.sh/golang-go-code-coverage-accurate/
…と結論だけなら簡単だが、ここに辿り着くまでに大層時間かかった。
I found a brutal hack of a workaround. I'm trying to report coverage on a project with a single golang module, and all of the module's files are below a single subdirectory in the git repo. We use go-acc to measure coverage, it writes file coverage.txt. I added a step in the build script to rewrite the prefix on the paths in that file, change the module name to the directory name, kinda like this:
sed -i -e 's/^mymodule/ModuleSubdir/' coverage.txt
Then the sonar scanner is happy. I don't know if the coverage numbers are correct, but they ain't zero no morehttps://jira.sonarsource.com/browse/SONARSLANG-450
大本は、is not included in the project, ignoring coverage
を解決すべく探していた際に見つけた上記であるが、結果的にソースコードのCoverage登録そのものとは関係なく、別の問題を解決することになった。うーん。
なんかもうちょっと綺麗にしたい気分ではあるんだけど…。
というのと、情報が少なくてきついですね。
- 投稿日:2020-08-11T19:06:41+09:00
golang + sonarqube + codebuild
https://qiita.com/fake-deli-ca/items/e01a42a9d04cf0bab8c4
の後にJenkinsじゃなくてCodeBuildも対応してみたんだけど、異様にわかりにくくて、時間かかった…。結論
最初に結論から。
関係する箇所のみ抜粋してある。buildspec.ymlversion: 0.2 env: variables: GO_VERSION : "" PROJECT_VERSION : "" # 本来はParameter-Storeにすべき SONARQUBE_TOKEN : "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" phases: install: runtime-versions: golang: ${GO_VERSION} commands: - type go && go version - chmod +x ./gradlew - export REGOPATH=`echo $GOPATH | cut -c 5-` - export PATH=${REGOPATH}/bin:${PATH} && echo ${PATH} pre_build: commands: - echo 'setup' - make setup build: commands: - echo 'make & test' - make - make test - ./gradlew sonarqube -Dsonar.projectBaseDir=${REGOPATH}/src -Dsonar.login=${SONARQUBE_TOKEN} -Dsonar.projectVersion=${PROJECT_VERSION} --info下記のMakefileはBuildの記載がないので、このままでは動作しない。
.PHONY: all all: build .PHONY: setup setup: go get -u github.com/ory/go-acc .PHONY: test test: go test ./... -json > reports/test.json go-acc ./... -o reports/coverage.outbuild.gradleplugins { id "org.sonarqube" version "3.0" } sonarqube { properties { //FIXME property "sonar.host.url","https://sonarqube.foo.jp" property "sonar.projectKey", "hogehoge" property "sonar.projectName", "hogehoge Project" property "sonar.go.coverage.reportPaths", "(gitのpass)/reports/coverage.out" property "sonar.go.tests.reportPaths", "(gitのpass)/reports/test.json" //FIXME property "sonar.sources", "." property "sonar.exclusions", "**/*_test.go" property "sonar.tests", "." property "sonar.test.inclusions", "**/*_test.go" property "sonar.test.exclusions", "**/vendor/**" property "sonar.sourceEncoding", "UTF-8" } }確認すべき点
is not included in the project, ignoring coverage
Sensor Go Cover sensor for Go coverage [go] Load coverage report from '/codebuild/output/src532592822/src/(gitのpass)/coverage.txt' File '/codebuild/output/src532592822/src/(gitのpass)/AAA/BBB.go' is not included in the project, ignoring coverageとにかく、
is not included in the project, ignoring coverage
にやられた。どう設定していいのか全く分からない。
https://github.com/SonarSource/sonar-go/issues/218
を読むとGOPATHの設定が悪い、と書いてあるが、どうすれば治るのかは書いてない。
酷い…。結論を言うと、
sonar.projectBaseDir
の設定が悪い。ところが、皆様ご存じの通り、Codebuildは/codebuild/output/srcXXXXXXXXXX/src
の部分が毎回異なるため、何らかの利用できる変数で設定しなければならない。
普通に考えるとsonar.projectBaseDir=$GOPATH/src
とすればよいのだが、ところがである。Codebuildの$GOPATH
は/go:/codebuild/output/src532592822
となっており、/go: の部分が心底余計なのである。
上記のように設定すると、* What went wrong: Execution failed for task ':sonarqube'. > The folder '/go:/codebuild/output/src179970974/src' does not exist for 'xxxxx' (base directory = /codebuild/output/src179970974/src/(gitのpass))みたいなエラーになるのである。まあ当然だが。
というわけで強引にcutで削除して再設定した。本当にこれでいいのかどうかは知らない。
また、本来はbuild.gradleに全部書きたかったのだが、当然環境変数と展開できないので、-Dsonar.projectBaseDir=${REGOPATH}/src
という設定とした。reportPathsの変更
上記projectBaseDirの変更に伴う対応である。
普通、この辺のPathは$CODEBUILD_SRC_DIR
の直下に置かれるので、gitのpassは本来不要なのであるが、projectBaseDirを変更した関係上、ここも変更が必要である。property "sonar.go.coverage.reportPaths", "(gitのpass)/reports/coverage.out" property "sonar.go.tests.reportPaths", "(gitのpass)/reports/test.json"Gradleの利用
sonarqubeだけgradle使っててなんかおかしな感じではあるのだが、最初こんな感じでやってたらunzipのインストールが必要だったり、sonar-scannerのインストール位置が気持ち悪かったり、なんだか訳がわからなくなりそうだったので、あきらめて素直にgradle使うことにした。別に使わなくてもいいとは思う。ただ↓の設定でうまくいくところまでは試してない。少なくともGOPATHはこのままでは駄目だと思う。
また、gogradleで全部書き直すことも考えたが、なんかそこまでして綺麗にする理由もなかったのでやめ。buildspec.ymlphases: install: runtime-versions: golang: ${GO_VERSION} commands: - type go && go version # sonar-scanner install - yum -y install unzip - wget https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.4.0.2170-linux.zip - unzip ./sonar-scanner-cli-4.4.0.2170-linux.zip pre_build: commands: - echo 'setup' - export PATH=${GOPATH}/bin:./sonar-scanner-4.4.0.2170-linux/bin:${PATH} && echo ${PATH} - make setup build: commands: - echo 'make & test' - make - make test - sonar-scanner -X -Dsonar.login=${SONARQUBE_TOKEN}go-accの利用
ここまで実施してみて、どうもCoverage率が異様に低いことに気が付く。
これも結論だけをいうと、go test -cover
のデフォルト挙動に問題があるということである。なので、go-acc
を利用することとした。The reason for this was that go test -cover is per default only recording code coverage for the package that is currently tested, not for all the packages from the project. If you have a lot of integration tests, your code coverage might be higher than you think!
https://www.ory.sh/golang-go-code-coverage-accurate/
…と結論だけなら簡単だが、ここに辿り着くまでに大層時間かかった。
I found a brutal hack of a workaround. I'm trying to report coverage on a project with a single golang module, and all of the module's files are below a single subdirectory in the git repo. We use go-acc to measure coverage, it writes file coverage.txt. I added a step in the build script to rewrite the prefix on the paths in that file, change the module name to the directory name, kinda like this:
sed -i -e 's/^mymodule/ModuleSubdir/' coverage.txt
Then the sonar scanner is happy. I don't know if the coverage numbers are correct, but they ain't zero no morehttps://jira.sonarsource.com/browse/SONARSLANG-450
大本は、is not included in the project, ignoring coverage
を解決すべく探していた際に見つけた上記であるが、結果的にソースコードのCoverage登録そのものとは関係なく、別の問題を解決することになった。うーん。
なんかもうちょっと綺麗にしたい気分ではあるんだけど…。
というのと、情報が少なくてきついですね。
- 投稿日:2020-08-11T18:20:22+09:00
【Go】ユニットテスト
下記のコースのアウトプットになります。
Unit testing for Go developers | Udemy参考にした記事
Goでテストを書く(テストの実装パターン集) - QiitaGolang のテスト
実行コマンド
go test ./...
詳細を知りたい場合は下記
go test -v ./...
メッセージ
TODO
Table driven Test
Table Driven Testとは、それぞれの行が入力と期待する出力を含んだテストケースになっているテーブルを作り、それをループして実行する形式のテストです。
DMMにおけるユーザーレビュー基盤の変革(Goのテスト技法編) - DMM insideテスト対象のコード
package calculator import "errors" type DiscountCalculator struct { minimumPurchaseAmount int discountAmount int } func NewDiscountCalculator(minimumPurchaseAmount int, discountAmount int) (*DiscountCalculator, error) { // discountRepository database.Discount if minimumPurchaseAmount == 0 { return &DiscountCalculator{}, errors.New("minimum purchase amount could not be zero") } return &DiscountCalculator{ minimumPurchaseAmount: minimumPurchaseAmount, discountAmount: discountAmount, }, nil } func (c *DiscountCalculator) Calculate(purchaseAmount int) int { if purchaseAmount > c.minimumPurchaseAmount { multiplier := purchaseAmount / c.minimumPurchaseAmount return purchaseAmount - (c.discountAmount * multiplier) } return purchaseAmount }下記でもわかる通り、テストデータが非常にわかりやすいです。
package calculator import "testing" // testingの宣言は下記のような感じ func TestDiscountCalculator(t *testing.T) { type testCase struct { name string minimumPurchaseAmount int discount int purchaseAmount int exepectedAmount int } // テストケーズをまとめているところ // テストケースが一目でわかりやすい testCases := []testCase{ {name: "should apply 20", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 150, exepectedAmount: 130}, {name: "should apply 40", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 200, exepectedAmount: 160}, {name: "should apply 60", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 350, exepectedAmount: 290}, {name: "should not apply", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 50, exepectedAmount: 50}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { calculator := NewDiscountCalculator(tc.minimumPurchaseAmount, tc.discount) amount := calculator.Calculate(tc.purchaseAmount) if amount != tc.exepectedAmount { t.Errorf("exepected %v, got %v", tc.exepectedAmount, amount) } }) } }// 実行結果 === RUN TestDiscountCalculator === RUN TestDiscountCalculator/should_apply_20 === RUN TestDiscountCalculator/should_apply_40 === RUN TestDiscountCalculator/should_apply_60 === RUN TestDiscountCalculator/should_not_apply --- PASS: TestDiscountCalculator (0.00s) --- PASS: TestDiscountCalculator/should_apply_20 (0.00s) --- PASS: TestDiscountCalculator/should_apply_40 (0.00s) --- PASS: TestDiscountCalculator/should_apply_60 (0.00s) --- PASS: TestDiscountCalculator/should_not_apply (0.00s)golangでテストを書くならテーブルドリブンテスト - Qiita
例外処理
部分的にテストをする場合は、
go test -v
Assert
assert.Equal(t, tc.exepectedAmount, amount)
Mock(モック)
下記記事が一番わかりやすいです。
testify/mockでgolangのテストを書く - Qiita// Mock の宣言 type DiscountRepositoryMock struct { mock.Mock } // interface 宣言 type Discount interface { FindCurrentDiscount() int } // Mockを使ったメソッドの呼び出し func (drm DiscountRepositoryMock) FindCurrentDiscount() int { // 条件にあったReturn設定値を返す args := drm.Called() return args.Int(0) } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Mockを初期化 discountRepositoryMock := DiscountRepositoryMock{} // 使用するメソッドと返り値を設定する discountRepositoryMock.On("FindCurrentDiscount").Return(tc.discount) calculator, err := NewDiscountCalculator(tc.minimumPurchaseAmount, discountRepositoryMock) if err != nil { // FailNow + log t.Fatalf("could not instantiate the calculator %s", err.Error()) } amount := calculator.Calculate(tc.purchaseAmount) assert.Equal(t, tc.exepectedAmount, amount) }) }
- 投稿日:2020-08-11T18:20:22+09:00
【Go】ユニットテストのMock、Table driven Testとか
下記のコースのアウトプットになります。
Unit testing for Go developers | Udemy参考にした記事
Goでテストを書く(テストの実装パターン集) - QiitaGolang のテスト
実行コマンド
go test ./...
詳細を知りたい場合は下記
go test -v ./...
メッセージ
TODO
Table driven Test
Table Driven Testとは、それぞれの行が入力と期待する出力を含んだテストケースになっているテーブルを作り、それをループして実行する形式のテストです。
DMMにおけるユーザーレビュー基盤の変革(Goのテスト技法編) - DMM insideテスト対象のコードです。
下記からは、コースで使用したソースを使っていきます。package calculator import "errors" type DiscountCalculator struct { minimumPurchaseAmount int discountAmount int } func NewDiscountCalculator(minimumPurchaseAmount int, discountAmount int) (*DiscountCalculator, error) { if minimumPurchaseAmount == 0 { return &DiscountCalculator{}, errors.New("minimum purchase amount could not be zero") } return &DiscountCalculator{ minimumPurchaseAmount: minimumPurchaseAmount, discountAmount: discountAmount, }, nil } func (c *DiscountCalculator) Calculate(purchaseAmount int) int { if purchaseAmount > c.minimumPurchaseAmount { multiplier := purchaseAmount / c.minimumPurchaseAmount return purchaseAmount - (c.discountAmount * multiplier) } return purchaseAmount }下記でもわかる通り、テストデータが非常にわかりやすいです。
package calculator import "testing" // testingの宣言は下記のような感じ func TestDiscountCalculator(t *testing.T) { type testCase struct { name string minimumPurchaseAmount int discount int purchaseAmount int exepectedAmount int } // テストケーズをまとめているところ // テストケースが一目でわかりやすい testCases := []testCase{ {name: "should apply 20", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 150, exepectedAmount: 130}, {name: "should apply 40", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 200, exepectedAmount: 160}, {name: "should apply 60", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 350, exepectedAmount: 290}, {name: "should not apply", minimumPurchaseAmount: 100, discount: 20, purchaseAmount: 50, exepectedAmount: 50}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { calculator := NewDiscountCalculator(tc.minimumPurchaseAmount, tc.discount) amount := calculator.Calculate(tc.purchaseAmount) if amount != tc.exepectedAmount { t.Errorf("exepected %v, got %v", tc.exepectedAmount, amount) } }) } }// 実行結果 === RUN TestDiscountCalculator === RUN TestDiscountCalculator/should_apply_20 === RUN TestDiscountCalculator/should_apply_40 === RUN TestDiscountCalculator/should_apply_60 === RUN TestDiscountCalculator/should_not_apply --- PASS: TestDiscountCalculator (0.00s) --- PASS: TestDiscountCalculator/should_apply_20 (0.00s) --- PASS: TestDiscountCalculator/should_apply_40 (0.00s) --- PASS: TestDiscountCalculator/should_apply_60 (0.00s) --- PASS: TestDiscountCalculator/should_not_apply (0.00s)golangでテストを書くならテーブルドリブンテスト - Qiita
例外処理
TODO
Assert
assert.Equal(t, tc.exepectedAmount, amount)
Mock(モック)
Mockは、必要なテスト条件を容易に作ることができます。
Mockでユニットテストを簡単にしよう! | GMOアドパートナーズグループ TECH BLOG byGMOGolangでは、下記記事が一番わかりやすかったです。
testify/mockでgolangのテストを書く - Qiita// Mock の宣言 type DiscountRepositoryMock struct { mock.Mock } // interface 宣言 type Discount interface { FindCurrentDiscount() int } // Mockを使ったメソッドの呼び出し func (drm DiscountRepositoryMock) FindCurrentDiscount() int { // 条件にあったReturn設定値を返す args := drm.Called() return args.Int(0) } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Mockを初期化 discountRepositoryMock := DiscountRepositoryMock{} // 使用するメソッドと返り値を設定する discountRepositoryMock.On("FindCurrentDiscount").Return(tc.discount) calculator, err := NewDiscountCalculator(tc.minimumPurchaseAmount, discountRepositoryMock) if err != nil { // FailNow + log t.Fatalf("could not instantiate the calculator %s", err.Error()) } amount := calculator.Calculate(tc.purchaseAmount) assert.Equal(t, tc.exepectedAmount, amount) }) }
- 投稿日:2020-08-11T18:17:36+09:00
GoでバイナリからJPEG生成
Goでバイナリでファイルを受け取った際にそのまま加工したい時
結論
bytes パッケージを使おう
Go言語 パッケージ bytesソースコード
main.gopackage main import ( "bytes" "fmt" "image" "image/jpeg" "io/ioutil" "os" ) func main() { // ファイルの読み込み buf, _ := ioutil.ReadFile("input.jpg") // バイナリファイルをReaderに読み込み reader := bytes.NewReader(buf) // 画像変換 img, _, err := image.Decode(reader) if err != nil { fmt.Errorf("err %v", err) } // ファイル作成 f, err := os.Create("output.jpg") if err != nil { fmt.Errorf("err %v", err) } defer f.Close() opt := jpeg.Options{ Quality: 90, } // 今回はJPEGだがPNGとかでもできそう if err = jpeg.Encode(f, img, &opt); err != nil { fmt.Errorf("err %v", err) } }参考
- 投稿日:2020-08-11T17:08:30+09:00
Golang 基礎 (struct周り)part2
part1の続き
makeとnewの違い
どちらも値を入れずメモリ領域を確保する。違いはポインタが返ってくるかどうか。
// 返ってこない s := make([]int, 0) m := make(map[string]int) ch := make(chan int) // 返ってくる var p *int = new(int) var st = new(struct{})cf. 中身はnil (メモリ確保していない)
var s []int var m map[string]int var ch chan int var p *intstruct
type Vertex struct { X, Y int S string }v1とv2は同じ意味。v3とv4は同じ意味。
v1 := Vertex{} // {0 0 } var v2 Vertex // {0 0 } v3 := new(Vertex) // &{0 0 } v4 := &Vertex{} // &{0 0 }changeVertex := func(v Vertex) { v.X = 1000 } changeVertex2 := func(v *Vertex) { v.X = 1000 // (*v).X = 1000 と同じ意味になる } v := Vertex{1, 2, "test"} changeVertex(v) // 値渡し fmt.Println(v) // {1 2 test} 値が書き換わっていない changeVertex2(&v) // 参照渡し fmt.Println(v) // {1000 2 test} 値が書き換わっているstructオリエンテッド
package main import "fmt" type Vertex struct { x, y int // 先頭が小文字だとpackage内からのみ参照可能 } // メソッド(値レシーバー) func (v Vertex) Area() int { return v.x * v.y } // メソッド(ポインタレシーバー) 中身を書き換える! func (v *Vertex) Scale(i int) { v.x = v.x * i v.y = v.y * i } // Embedded(継承みたいなやつ) type Vertex3D struct { Vertex z int } func (v Vertex3D) Area3D() int { return v.x * v.y * v.z } func (v *Vertex3D) Scale3D(i int) { v.x *= i v.y *= i v.z *= i } // コンストラクタ func New(x, y, z int) *Vertex3D { return &Vertex3D{Vertex{x, y}, z} } func main() { v := New(4, 5, 6) v.Scale3D(10) fmt.Println(v.Area()) // 継承元のメソッドも使える fmt.Println(v.Area3D()) }non-struct
自分なりの型とメソッドを作れる。
type MyInt int func (i MyInt) Double() int { return int(i * 2) }インターフェースとダックタイピング
package main import "fmt" // 設計書みたいなイメージ type Human interface { Say() string // 実装しないといけない関数を書く } type Person struct { Name string } func (p Person) Say() string { return "Mr." + p.Name } // インターフェースのダックタイピング(Say()というメソッドがないと成立しないときに使う) func DriveCar(human Human) { if human.Say() == "Mr.Mike" { fmt.Println("Run") } else { fmt.Println("Get out") } } func main() { var mike Human = Person{"Mike"} // Humanにstructを入れたら、Say()というメソッドがないとだめ DriveCar(mike) }タイプアサーションとswitch type文
// 引数が interface{} →何でも引数として受け入れる! func do(i interface{}) { /* タイプアサーション: interfaceを他の型に変える ii := i.(int) ss := i.(string) など */ // switch type 文 switch v := i.(type) { case int: fmt.Println(v * 2) case string: fmt.Println(v + "!!!!") default: fmt.Printf("I don't know %T.\n", v) } }Stringer
package main import "fmt" type Person struct { Name string Age int } // String()のメソッドを実装すれば fmt の出力を変えることができる! func (p Person) String() string { return fmt.Sprintf("My name is %v, I'm %v years old.", p.Name, p.Age) } func main() { mike := Person{"Mike", 20} fmt.Println(mike) // My name is Mike, I'm 20 years old. }カスタムエラー
package main import "fmt" // 自分なりのエラーを作る type UserNotFound struct { Username string } // 作ったエラーにError()のメソッドを実装するとエラー時にこれが出力される func (e *UserNotFound) Error() string { return fmt.Sprintf("User not found: %v", e.Username) } func myFunc() error { // something wrong ok := false if ok { return nil } return &UserNotFound{Username: "mike"} } func main() { if err := myFunc(); err != nil{ fmt.Println(err) // User not found: mike } }Error()をポインタレシーバーにする理由:エラーが違う場所で起きたときに比較判定するため。
以下サンプルコード。err == io.EOFの部分。
err == errors.New("EOF")としてもうまく行かない。→ r.Read()が io.EOFというエラーを返すため。package main import ( "fmt" "io" "strings" ) func main() { r := strings.NewReader("Hello, Reader!") b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n=%v err=%v b=%v\n", n, err, b) fmt.Printf("b[:n]=%q\n", b[:n]) if err == io.EOF{ break } } }参考
- 投稿日:2020-08-11T17:08:30+09:00
Golang 基礎 メモ part2
part1の続き
makeとnewの違い
どちらも値を入れずメモリ領域を確保する。違いはポインタが返ってくるかどうか。
// 返ってこない s := make([]int, 0) m := make(map[string]int) ch := make(chan int) // 返ってくる var p *int = new(int) var st = new(struct{})cf. 中身はnil (メモリ確保していない)
var s []int var m map[string]int var ch chan int var p *intstruct
type Vertex struct { X, Y int S string }v1とv2は同じ意味。v3とv4は同じ意味。
v1 := Vertex{} // {0 0 } var v2 Vertex // {0 0 } v3 := new(Vertex) // &{0 0 } v4 := &Vertex{} // &{0 0 }changeVertex := func(v Vertex) { v.X = 1000 } changeVertex2 := func(v *Vertex) { v.X = 1000 // (*v).X = 1000 と同じ意味になる } v := Vertex{1, 2, "test"} changeVertex(v) // 値渡し fmt.Println(v) // {1 2 test} 値が書き換わっていない changeVertex2(&v) // 参照渡し fmt.Println(v) // {1000 2 test} 値が書き換わっているstructオリエンテッド
package main import "fmt" type Vertex struct { x, y int // 先頭が小文字だとpackage内からのみ参照可能 } // メソッド(値レシーバー) func (v Vertex) Area() int { return v.x * v.y } // メソッド(ポインタレシーバー) 中身を書き換える! func (v *Vertex) Scale(i int) { v.x = v.x * i v.y = v.y * i } // Embedded(継承みたいなやつ) type Vertex3D struct { Vertex z int } func (v Vertex3D) Area3D() int { return v.x * v.y * v.z } func (v *Vertex3D) Scale3D(i int) { v.x *= i v.y *= i v.z *= i } // コンストラクタ func New(x, y, z int) *Vertex3D { return &Vertex3D{Vertex{x, y}, z} } func main() { v := New(4, 5, 6) v.Scale3D(10) fmt.Println(v.Area()) // 継承元のメソッドも使える fmt.Println(v.Area3D()) }non-struct
自分なりの型とメソッドを作れる。
type MyInt int func (i MyInt) Double() int { return int(i * 2) }インターフェースとダックタイピング
package main import "fmt" // 設計書みたいなイメージ type Human interface { Say() string // 実装しないといけない関数を書く } type Person struct { Name string } func (p Person) Say() string { return "Mr." + p.Name } // インターフェースのダックタイピング(Say()というメソッドがないと成立しないときに使う) func DriveCar(human Human) { if human.Say() == "Mr.Mike" { fmt.Println("Run") } else { fmt.Println("Get out") } } func main() { var mike Human = Person{"Mike"} // Humanにstructを入れたら、Say()というメソッドがないとだめ DriveCar(mike) }タイプアサーションとswitch type文
// 引数が interface{} →何でも引数として受け入れる! func do(i interface{}) { /* タイプアサーション: interfaceを他の型に変える ii := i.(int) ss := i.(string) など */ // switch type 文 switch v := i.(type) { case int: fmt.Println(v * 2) case string: fmt.Println(v + "!!!!") default: fmt.Printf("I don't know %T.\n", v) } }Stringer
package main import "fmt" type Person struct { Name string Age int } // String()のメソッドを実装すれば fmt の出力を変えることができる! func (p Person) String() string { return fmt.Sprintf("My name is %v, I'm %v years old.", p.Name, p.Age) } func main() { mike := Person{"Mike", 20} fmt.Println(mike) // My name is Mike, I'm 20 years old. }カスタムエラー
package main import "fmt" // 自分なりのエラーを作る type UserNotFound struct { Username string } // 作ったエラーにError()のメソッドを実装するとエラー時にこれが出力される func (e *UserNotFound) Error() string { return fmt.Sprintf("User not found: %v", e.Username) } func myFunc() error { // something wrong ok := false if ok { return nil } return &UserNotFound{Username: "mike"} } func main() { if err := myFunc(); err != nil{ fmt.Println(err) // User not found: mike } }Error()をポインタレシーバーにする理由:エラーが違う場所で起きたときに比較判定するため。
以下サンプルコード。err == io.EOFの部分。
err == errors.New("EOF")としてもうまく行かない。→ r.Read()が io.EOFというエラーを返すため。package main import ( "fmt" "io" "strings" ) func main() { r := strings.NewReader("Hello, Reader!") b := make([]byte, 8) for { n, err := r.Read(b) fmt.Printf("n=%v err=%v b=%v\n", n, err, b) fmt.Printf("b[:n]=%q\n", b[:n]) if err == io.EOF{ break } } }参考
- 投稿日:2020-08-11T06:13:51+09:00
Golang 基礎 メモ part1
パッケージ管理
ターミナルで実行。go.modファイルが作成される。
go mod init github.com/${GITHUB_USER}/${PROJECT_NAME}あとは go get するだけ。
実行環境(docker)
main.goがあるディレクトリに以下を置く。(go.sumは1回でもgo getしてる場合のみCOPYする)
DockerfileFROM golang:alpine WORKDIR /go/src/app COPY go.mod . # COPY go.sum . RUN go mod download COPY . .docker-compose.ymlversion: "3.7" services: goapp: build: . tty: true volumes: - .:/go/src/app以下のコマンドでコンテナの中に入る。(goappはサービス名)
~% docker-compose up -d ~% docker-compose exec goapp /bin/ashコンテナ内でmain.goを実行。
/go/src/app # go run main.gostrconv パッケージ
string型と基本的なデータ型との間の変換をするパッケージ
i, err := strconv.Atoi("-42") // stringからintへ s := strconv.Itoa(-42) // intからstringへスライスのmakeとcap
cap: メモリに確保してる分
m := make([]int, 5) n := make([]int, 0, 5) fmt.Printf("len=%d cap=%d value=%v\n", len(m), cap(m), m) // len=5 cap=5 value=[0 0 0 0 0] fmt.Printf("len=%d cap=%d value=%v\n", len(n), cap(n), n) // len=0 cap=5 value=[]s1 := make([]int, 0) // 空のスライスをメモリに確保する(mapも同様) var s2 []int // nil(メモリに確保しない。mapも同様)map(辞書型)
m := map[string]int{"apple": 100, "banana": 200} fmt.Println(m["nothing"]) // 0 v, ok := m["apple"] // 2つ目の返り値は受け取らなくてもおっけい fmt.Println(v, ok) // 100 true可変長引数
func foo(params ...int) { fmt.Printf("len=%d params=%v\n" , len(params), params) for _, param := range params { fmt.Println(param) } } func main() { foo() // len=0 params=[] foo(1, 2) // len=2 params=[1 2] s := []int{1, 2, 3} foo(s...) // len=3 params=[1 2 3] }forとrange
// 条件のみ sum := 1 for sum < 500 { sum += sum } fmt.Println(sum) // 無限ループ for { fmt.Println("hello") } // range l := []string{"python", "go", "java"} for index, value := range l{ fmt.Println(index, value) } m := map[string]int{"apple": 100, "banan": 200} for key, value := range m{ fmt.Println(key, value) } for key := range m{ fmt.Println(key) } for _, value := range m{ fmt.Println(value) }swicth
// 通常 os := "mac" switch os { case "windows": fmt.Println("windows!!") case "mac": fmt.Println("mac!!") default: fmt.Println("linux!!") } // 条件省略 t := time.Now() switch { case t.Hour() < 12: fmt.Println("morning") case t.Hour() < 17: fmt.Println("afternoon") default: fmt.Println("foooo") }panicとrecover
panicで強制終了してしまう処理の前にdeferでrecover()すると強制終了しなくなる!
func thirdPartyConnectDB() { panic("Unable to connect database.") } func save() { defer func() { s := recover() fmt.Println(s) }() thirdPartyConnectDB() } func main() { save() fmt.Println("ok?") }参考
- 投稿日:2020-08-11T06:13:51+09:00
Golang 基礎 (環境構築など) part1
前準備や基本情報
パッケージ管理
ターミナルで実行。go.modファイルが作成される。
go mod init github.com/${GITHUB_USER}/${PROJECT_NAME}あとは go get するだけ。
実行環境(docker)
main.goがあるディレクトリに以下を置く。(go.sumは1回でもgo getしてる場合のみCOPYする)
DockerfileFROM golang:alpine WORKDIR /go/src/app COPY go.mod . # COPY go.sum . RUN go mod download COPY . .docker-compose.ymlversion: "3.7" services: goapp: build: . tty: true volumes: - .:/go/src/app以下のコマンドでコンテナの中に入る。(goappはサービス名)
~% docker-compose up -d ~% docker-compose exec goapp /bin/ashコンテナ内でmain.goを実行。
/go/src/app # go run main.gopublicとprivate
- 命名規則で判別。(変数名、関数名など)そのため、基本的にキャピタルケースorパスカルケースで命名する。
- public: 他のパッケージからでも呼び出せる。先頭を大文字にする!
- private: パッケージ内でのみ呼び出せる。先頭は小文字。
var PublicVariable string = "Public" // 呼び出せる var privateVariable string = "private" // 呼び出せない
- ファイル名はスネークケース ex. line_api.go
基礎文法
strconv パッケージ
string型と基本的なデータ型との間の変換をするパッケージ
i, err := strconv.Atoi("-42") // stringからintへ s := strconv.Itoa(-42) // intからstringへスライスのmakeとcap
cap: メモリに確保してる分
m := make([]int, 5) n := make([]int, 0, 5) fmt.Printf("len=%d cap=%d value=%v\n", len(m), cap(m), m) // len=5 cap=5 value=[0 0 0 0 0] fmt.Printf("len=%d cap=%d value=%v\n", len(n), cap(n), n) // len=0 cap=5 value=[]s1 := make([]int, 0) // 空のスライスをメモリに確保する(mapも同様) var s2 []int // nil(メモリに確保しない。mapも同様)map(辞書型)
m := map[string]int{"apple": 100, "banana": 200} fmt.Println(m["nothing"]) // 0 v, ok := m["apple"] // 2つ目の返り値は受け取らなくてもおっけい fmt.Println(v, ok) // 100 true可変長引数
func foo(params ...int) { fmt.Printf("len=%d params=%v\n" , len(params), params) for _, param := range params { fmt.Println(param) } } func main() { foo() // len=0 params=[] foo(1, 2) // len=2 params=[1 2] s := []int{1, 2, 3} foo(s...) // len=3 params=[1 2 3] }forとrange
// 条件のみ sum := 1 for sum < 500 { sum += sum } fmt.Println(sum) // 無限ループ for { fmt.Println("hello") } // range l := []string{"python", "go", "java"} for index, value := range l{ fmt.Println(index, value) } m := map[string]int{"apple": 100, "banan": 200} for key, value := range m{ fmt.Println(key, value) } for key := range m{ fmt.Println(key) } for _, value := range m{ fmt.Println(value) }swicth
// 通常 os := "mac" switch os { case "windows": fmt.Println("windows!!") case "mac": fmt.Println("mac!!") default: fmt.Println("linux!!") } // 条件省略 t := time.Now() switch { case t.Hour() < 12: fmt.Println("morning") case t.Hour() < 17: fmt.Println("afternoon") default: fmt.Println("foooo") }panicとrecover
panicで強制終了してしまう処理の前にdeferでrecover()すると強制終了しなくなる!
func thirdPartyConnectDB() { panic("Unable to connect database.") } func save() { defer func() { s := recover() fmt.Println(s) }() thirdPartyConnectDB() } func main() { save() fmt.Println("ok?") }参考
現役シリコンバレーエンジニアが教えるGo入門 + 応用でビットコインのシストレFintechアプリの開発
https://xblood.hatenablog.com/entry/2019/01/28/213223
- 投稿日:2020-08-11T00:48:20+09:00
ProxyConnectHeaderを使ってhttp.ClientによるCONNECTメソッドにヘッダーを付加する
事象
Go 1.14.2の環境で、認証Proxyを経由して任意のサイトにアクセスするHTTPクライアントを実装していたところ、接続先がHTTPSのときに認証proxyから407が返ってしまいました。
再現コード(main.go)func main() { u, _ := url.Parse("http://127.0.0.1:3128") // ローカルにsquidを立て、認証Proxyとした req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil) if err != nil { log.Fatal(err) } req.Header.Add("Proxy-Authorization", "Basic <basic auth string>") c := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(u), }, } resp, err := c.Do(req) if err != nil { log.Fatal(err) } fmt.Println(resp.Status) }上記を実行すると、
407 (Proxy Authentication Required)
が返って通信ができません。Proxy-Authorization
に然るべき認証情報は正しくセットしているのですが、なぜ通信できないのでしょうか。$ go run main.go 2020/08/09 16:02:34 Get https://www.yahoo.co.jp/: Proxy Authentication Required exit status 1原因
接続先がHTTPSの場合、接続先とのTLSハンドシェイクの前に、仲介者である認証Proxyに対して
CONNECT
メソッドが発行されます。再現コードでヘッダーとして付与していたProxy-Authorization
が、このCONNECTメソッド実行時に付与されなかったため、本事象が発生していました。CONNECTの実行は、内部的には
http.Transport
のdialConn
メソッドにて実行されています。このメソッドでは、CONNECTを行うためにhttp.Request
を新たに生成していますが、ここではもともと再現コードでProxy-AuthorizationをセットしたHeaderを使いません。その代わり、http.Transport
のProxyConnectHeader
という項目の値をセットしています。http.Transport#dialConnより抜粋(transport.go#L1564-L1579)case cm.targetScheme == "https": conn := pconn.conn hdr := t.ProxyConnectHeader if hdr == nil { hdr = make(Header) } if pa := cm.proxyAuth(); pa != "" { hdr = hdr.Clone() hdr.Set("Proxy-Authorization", pa) } connectReq := &Request{ Method: "CONNECT", URL: &url.URL{Opaque: cm.targetAddr}, Host: cm.targetAddr, Header: hdr, }このCONNECT時に元のHeaderを使わない仕様、私は最初ちょっと意外に感じたのですが、考えてみれば、元々の要求でセットしていたHeaderはあくまで本来の接続先のために用意しているものなので、認証Proxyとの会話であるCONNECT時にそれを渡してはならないですね。不要ですし、何よりセキュリティの観点から望ましくないです。
ProxyConnectHeader
は、当然ながらhttp.Transport
のコメントでも説明されています。// ProxyConnectHeader optionally specifies headers to send to // proxies during CONNECT requests. ProxyConnectHeader Header // Go 1.8https://golang.org/pkg/net/http/#Transport
対応
http.Request.Header
にセットしていた値を、ProxyConnectHeader
に入れてあげるよう変更すれば、事象の解消が可能です。main.gofunc main() { u, _ := url.Parse("http://127.0.0.1:3128") req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil) if err != nil { log.Fatal(err) } hdr := make(http.Header) hdr.Add("Proxy-Authorization", "Basic <basic auth string>") c := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(u), ProxyConnectHeader: hdr, }, } resp, err := c.Do(req) if err != nil { log.Fatal(err) } fmt.Println(resp.Status) }いけましたね。
$ go run main.go 200 OK
また、
Proxy-Authorization
に固定の認証情報をセットしたいだけなら、ProxyのURLに入れても良いですね。func main() { u, _ := url.Parse("http://<username>:<password>@127.0.0.1:3128") // 認証情報を付加 req, err := http.NewRequest("GET", "https://yahoo.co.jp", nil) if err != nil { log.Fatal(err) } c := &http.Client{ Transport: &http.Transport{ Proxy: http.ProxyURL(u), }, } resp, err := c.Do(req) if err != nil { log.Fatal(err) } fmt.Println(resp.Status) }$ go run main.go 200 OK
おわり。