- 投稿日:2020-03-05T23:39:25+09:00
[golang] mockery/testify mockを作るのに困ったとき
mockery : https://github.com/vektra/mockery
testify: https://github.com/stretchr/testify
上記のモック作成ツールを使ったテストを書いているとき、mockを定義しようとして「困ったな」と思う事が時々あります。そんなときに使えそうな方法について書いてみました。それぞれの解答に対して評価(〇、△、X)をつけていますが、あくまでこれは主観的なものです。異見があるかたはコメントください。
問題1(副作用)
さて、golangでこんなモジュールがあったとします。
type Sample struct { } func (s *Sample) Hoge(in interface{}, out interface{}) error { // do something }in に入力を受け取り、outに出力内容を書きこんで返してくるメソッドです。
たとえばinを
json.Marshal
して作成したbodyを使ってhttpリクエストし、戻ってきたレスポンスをjson.Unmarshal
でoutに書き込んで返してくるイメージ。さて、これをモックするとします。
mockSample := new(mocks.Sample) in := &Input{something} mockSample.On("Hoge", in, mock.Anything).Return(nil)ここで困ってしまいます。Hogeはoutの中身を書き換えて返す関数ですが、これをどうモックで表現したらいいんでしょう?
解答1(X)
mockSample := new(mocks.Sample) in := &Input{something} mockSample.On("Hoge", in, mock.Anything).Return(nil)何もしない。もちろんテストがそもそも通過しません。
解答2(〇)
mockSample.On("Hoge", in, out).Return(nil).Once().Run(func(args mocks.Argument) { o := args[2].(*Output) *o = Output{something} })こうすることでこのmockは引数に値を出力することができそうです。このRun()に渡している関数はRunFnと定義されているようですね。
何をしているかといいますと、Runの中ではmockがリクエストを受けたとき行う処理を書くことができます。そこで、引数として受け取ったoutのアドレスの参照先に戻したい値の「実体」をコピーしています。
戻り値で戻すならReturnに書くだけなので簡単ですが、引数による返却は少し難しいです・・・ですがargumentsを引数に受け取って何かする関数であるRunFnは、基本的にこのような事を目的とするもののように思われます。
解答3(X)
mockSample.On("Hoge", in, mock.MatchedBy(func(out interface{}) bool) { o := out.(*Output) *o = Output{something} return true })).Return(nil)MatchedByを使ってこんなこともできそうではあるものの、MatchedByはあくまでmatcherなので、matcher以外の用途(副作用)に使うべきではないかと思います。
問題2(関数引数)
package sample type Sample interface { Hoge(func() string) string Fuga() string } type sample struct{} func (s *sample) Hoge(f func() string) string { return f() } func (s *sample) Fuga() string { return "Fuga" } type Sample2 struct { s Sample } func (s *Sample2) Piyo() string { return s.s.Hoge(func() string { return s.s.Fuga() }) }ちょっと長いのですが、
Hoge(func() string) string
こんなものをモックする方法についていろいろ試してみました。解答1(X)
m := new(mocks.Sample) m.On("Fuga").Return("Mock2") m.On("Hoge", mock.Anything).Return("Mock")もちろんこれだけだとHogeの引数である関数をテストしていませんのでダメです。
解答2(△)
m := new(mocks.Sample) m.On("Fuga").Return("Mock2") m.On("Hoge", mock.Anything).Return("Mock").Run(func(args mock.Arguments){ f := args[0].(func() string) assert.Equal(t, "Mock2", f()) })モック定義の中でアサートしている事が違和感があります。
解答2(△)
m := new(mocks.Sample) m.On("Fuga").Return("Mock2") m.On("Hoge", mock.Anything).Return(func(f func() string) string { return f() })ReturnValueProviderFunctionです。
シンプルではあるものの、この関数、Hogeそのものなんです。「単体テスト」的に、テストの中でmockの実装に依存するのは抵抗があります。ただまあ、限りなく〇に近い△です。
解答3(X)
m := new(mocks.Sample) m.On("Fuga").Return("Mock2") m.On("Hoge", mock.MatchedBy(func(f func() string)bool{ fmt.Println(f()) return true })).Return("Mock")これはdeadlockになります。というのは、Matcherのなかでmock自身が呼ばれるとダメのようです。
RunやReturnValueProviderFunctionだとdeadlockにならないんですが・・・MatchedByは関数引数には無力です。
解答4(〇)
m := new(mocks.Sample) m.On("Fuga").Return("Mock2") m.On("Hoge", mock.Anything).Return("Mock") // test sut := &Sample2{m} assert.Equal(t, "Mock", sut.Piyo()) f := m.Calls[0].Arguments[0].(func()string) assert.Equal(t, "Mock", f())
- Anythingで荒くassertする
- そのあと、改めてargsのチェックをする
結論
いろいろ方法はあるかと思いますが、困ったときは「単体テストとは何か」に立ち返ってみるのが良いと思います。
そのうえで、RunやMatchedByやReturnValueProviderFunction、そしてテストそのものの分離を検討してみてください。
余
m.AssertExpectations()こんな記事を読むような方には言わずもがなと思いますが、コール数のチェックをお忘れなく!
参照
https://qiita.com/tomtwinkle/items/55f79c969d48206c9945
上記で @tomtwinkle さんがもっと具体的なコードを書いてくださってますので、こういう記事に興味があるかたはそちらも併せてどうぞ!
- 投稿日:2020-03-05T19:43:08+09:00
【GO】logrusでわかりやすいログを
ログ基盤
ログについてメモしていきます。
ログ設計基本事項
主に下記を参考にしています。
下記記事をみておけば、かなり理解が進みます!
ログ設計指針 - Qiita
開発者が運用を経験すべき一つの理由 | Developers.IO
PSR-3: Logger Interface - PHP-FIG気をつけること
DBの情報は記載しない
ログには「氏名」「住所」など特定の個人を識別する情報や「電話番号」や「メールアドレス」「SNSのアカウント情報」などインターネットにおいて個人と連絡を
FATALとERRORの違い
FATAL - アプリケーションを異常終了させるような非常に深刻なイベントを指定します。
ERROR - アプリケーションの稼働が継続できる程度のエラーを指定します。違いは、システム全体が継続できるかどうかかつシステムの部分が継続できるかどうかだと思っています。
logrus
Goでは、logrusという便利なmoduleを使用して実装をしました。
GitHub - sirupsen/logrus: Structured, pluggable logging for Go.
【Go×ログ】logrusの使い方を簡単に分かりやすくまとめてみた - Qiitapackage logger import ( "os" "github.com/sirupsen/logrus" ) var LOGGING *logrus.Entry func init() { logrus.SetOutput(os.Stdout) logrus.SetFormatter(&logrus.TextFormatter{ // ログをカラーで出力する ForceColors: true, FullTimestamp: true, }) switch os.Getenv("TEST_ENV") { case "production", "staging": logrus.SetLevel(logrus.InfoLevel) default: logrus.SetLevel(logrus.DebugLevel) } LOGGING = logrus.WithFields(logrus.Fields{}) } func Fatal(args ...interface{}) { logrus.Fatal(args...) } func Fatalf(format string, args ...interface{}) { logrus.Fatalf(format, args...) } func Error(args ...interface{}) { logrus.Error(args...) } func Errorf(format string, args ...interface{}) { logrus.Errorf(format, args...) } func Warn(args ...interface{}) { logrus.Warn(args...) } func Warnf(format string, args ...interface{}) { logrus.Warnf(format, args...) } func Info(args ...interface{}) { logrus.Info(args...) } func Infof(format string, args ...interface{}) { logrus.Infof(format, args...) } func Debug(args ...interface{}) { logrus.Debug(args...) } func Debugf(format string, args ...interface{}) { logrus.Debugf(format, args...) }Wrap
毎回logを吐く実装をするのも、きもわるいので、errorに内包しようと思いました。
使用例は下記の通りです。// Sprintfで変数と文字列を結合する if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("Failed to Get account test: %v", test)) }Go1.13のError wrappingを触ってみる - 逆さまにした
fmt.Printfなんかこわくない - Qiitaruntime
どこのファイルでかつどこのラインでエラーが出ているのかわかるようにしたいです。
そんな時はruntime
が非常に便利です。下記のようなファイルを作成して、
caller
として実行できるようにしました。package caller import ( "runtime" ) // ファイル名を取得するメソッド func GetCurrentFile() string { _, file, _, _ := runtime.Caller(1) return file } // 行数を取得するメソッド func GetCurrentFileLine() int { _, _, line, _ := runtime.Caller(1) return line }下記のように呼び出します。
if err != nil { return nil, errors.Wrap(err, fmt.Sprintf("Failed to Get account test: %v, file:%s line:%v", test, caller.GetCurrentFile(), caller.GetCurrentFileLine())) }[Go] ファイル名、行数、関数名、スタックトレースをランタイム時に取得する - YoheiM .NET
runtime.Caller(1)をなめて扱ったら危険かもしれない - Qiita
実行時の関数名やファイルパス、行数を取得する【Go】 - 技術向上その他
例外処理
「例外」がないからGo言語はイケてないとかって言ってるヤツが本当にイケてない件 - Qiita
fluentd仕組み
BufferedOutput pluginの代表的なoptionについて - Qiita
Fluentd ソースコード完全解説 (v0.10向け) · GitHubGCPでのfluentd
loggerには、fluentdからログが送られている。
配置は、daemonsetで全てのnodeに配置している。fluentdを導入時にまず知っておいたほうがよさそうなこと(インストール、監視、HA構成、チューニングなど) - Qiita
GKEではStackDriver Loggingにどうやってログを送っているか - Speaker Deck
Fluentdとはどのようなソフトウェアなのか - たごもりすメモ
- 投稿日:2020-03-05T19:23:46+09:00
初心者レベルでも気づく、JavaとGoのswitch文の記述の違い
基本的な
switch
文の記法Javaswitch rank { case 1: System.out.println("金メダル!"); break; case 2: System.out.println("銀メダル!"); break; case 3: System.out.println("銅メダル!"); break; default: System.out.println("メダルを獲得できませんでした"); break; }Goswitch rank { case 1: println("金メダル!") case 2: println("銀メダル!") case 3: println("銅メダル!") default: println("メダルを獲得できませんでした") }Javaの
switch
文の場合、暗黙的にfall-throughであるため、fall-throughされたくない場合はbreak
文を明示的に書く必要があります。一方、Goの
switch
文は、暗黙的にfall-throughでなく、明示的なbreak
文は必要ありません。複数条件のラベルを含む
switch
文Javaswitch n { case 0: System.out.println("凶です"); break; case 1: case 2: System.out.println("吉です"); break; case 3: case 4: System.out.println("中吉です"); break; case 5: System.out.println("大吉です"); break; }Goswitch n { case 0: println("凶です") case 1, 2: println("吉です") case 3, 4: println("中吉です") case 5: println("大吉です") }Goの
switch
文では、case
においてカンマ区切りで複数条件を一度に指定することができます。一方(JDK11までの)Javaの
switch
文では、「カンマ区切りで複数条件を一度に指定する」という記法は使えません。上記ソースコードのように、fall-throughを使って書く必要があります。結論
同じC-like languagesであっても、特に
switch
文の記法については、言語によって少なからぬ差が出ますね。
- 投稿日:2020-03-05T16:37:18+09:00
[Golang] gorilla/muxのCORS対処法
golangでサーバーを立てるときに、ルーターとして、gorilla/muxを使っている方もいると思います。gorilla/muxでCORSの問題を解決するとき、ハマってしまったので、まとめておきます。
このサイトを参考にして以下のようなミドルウェアを挿入してみましたが、No 'Access-Control-Allow-Origin' header is present on the requested resource.
エラーが出てしまいます。router.gopackage router import ( "fmt" "log" "net/http" "github.com/gorilla/mux" ) // CORSのためのミドルウェア func forCORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Headers", "*") w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") // プリフライトリクエストの対応 if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) return }) } func login(w http.ResponseWriter, r *http.Request) { // /loginに来た場合の処理を書く } func Init() { router := mux.NewRouter() //CORS対処のためのミドルウェア導入 router.Use(forCORS) router.HandleFunc("/login", login).Methods("POST") fmt.Println("Server Start...") log.Fatal(http.ListenAndServe(":8080", router)) }'Access-Control-Allow-Origin'をヘッダにセットしてるのにおかしいなと思いつつ、もしやgorilaのメソッド指定が悪影響を及ぼしているのか?と考え、外してみると、通りました。
func Init() { router := mux.NewRouter() //CORS対処のためのミドルウェア導入 router.Use(forCORS) //ここでメソッドの指定を外すとCORSエラーが解消された router.HandleFunc("/login", login) fmt.Println("Server Start...") log.Fatal(http.ListenAndServe(":8080", router)) }ライブラリの実装を読んで原因を探りたかったのですが、とりあえず早く先に進みたかったのでまだ読んでません。
このエラーで1日溶かしましたが、CORSについての知見が深まったのでいい経験を詰めた事にします。
- 投稿日:2020-03-05T13:41:25+09:00
obsのストリーミングをとりあえず動作確認
nginxじゃなくてGoのプログラムに向かってobsでストリーミングをしたかったので,Goのrtmpサーバーを探しました。
https://github.com/netroby/go-rtmp-server
かなりわかりやすく動作確認することができました!
動機
iOSアプリからRTMPでデータをストリームしたかったので,ちゃんと動くRTMPサーバーを手に入れてわかりやすく動作を確認しようと思ったのがきっかけでした。
OBSを入れる
https://obsproject.com/ja/download
僕はMacでやっているので,MacにOBSを入れます。GoのRTMPサーバーを導入する
go get -u -v github.com/netroby/go-rtmp-serverこれでサーバーを導入します。
go get
がちゃんと動作してくれないときは,Go自体の環境をつくればいいと思います。サーバーを起動する
~/go/bin/go-rtmp-server -l :8089 -k longSecurityKey
README
に書いてある通り,これでサーバーを起動することができます。
longSecurityKey
の部分はストリームキー
と呼ばれる,ストリーミングにおけるパスワードのようなものらしいので,自分の好きな文字列でもいいと思います。OBSから配信する
OBSの
設定
のところから配信
を選択し,サービス
でカスタム
を選択してから,
サーバーのところにrtmp://127.0.0.1/live?key=longSecurityKey
と入れます。(今回はローカルで動かします。)
そして設定を終え,入力ソースをちゃんと選択してから配信開始をします。配信を確認する
http://127.0.0.1:8089/
にブラウザからアクセスすると,obsからのストリームが受け取れていると思います!おわり
とりあえずこれでちゃんと動くであろうストリーミングのツールたちを手に入れることができたので,楽しく開発をすすめて行こうと思います。
- 投稿日:2020-03-05T02:07:31+09:00
【Go言語】FirebaseAuthのユーザーを全て削除する
概要
FirebaseAuthを検証で使ってたら
example1@example.org
みたいなユーザーが大量に増えてしまったので一括で削除を行いたいが、Firebaseのコンソールからは出来ないみたいだったので Firebase Admin SDK で一括削除を行うことにした。該当記事が見つけられなかったので自分用メモも兼ねて記事を書いた。今回は Go 言語で削除処理を書いたが、下記の言語も対応している模様。
- Node.js
- Java
- Python
- C#
参考: サーバーに Firebase Admin SDK を追加する
前準備
go get firebase.google.com/go
で Firebase Admin SDK を導入- Firebaseコンソールからサービスアカウントキーを取得する
サービス アカウント用の秘密鍵ファイルを生成するには:
1. Firebase コンソールで、[設定] > [サービス アカウント] を開きます。 2. [新しい秘密鍵の生成] をクリックし、[キーを生成] をクリックして確定します。 3. キーを含む JSON ファイルを安全に保管します。 ※ https://firebase.google.com/docs/admin/setup より引用実装
全ユーザーを一括削除するAPIは存在しなかったので、全ユーザーを取得してイテレータで1つずつ削除処理を行った。
参考: ユーザー管理 | Firebasepackage main import ( "context" "firebase.google.com/go" "firebase.google.com/go/auth" "google.golang.org/api/iterator" "google.golang.org/api/option" "log" ) func main() { // Firebaseと接続する opt := option.WithCredentialsFile("./サービスアカウントキー.json") app, err := firebase.NewApp(context.Background(), nil, opt) if err != nil { log.Fatalf("error connect firebase: %s\n", err) } // FirebaseAuthクライアントを取得 client, err := app.Auth(context.Background()) if err != nil { log.Fatalf("error connect firebase auth: %s\n", err) } // 全てのユーザーを取得する iter := client.Users(context.Background(), "") for { user, err := iter.Next() if err == iterator.Done { break } if err != nil { log.Fatalf("error listing users: %s\n", err) } log.Printf("read user email: %v\n", user.Email) // ユーザーを削除する err = deleteUser(client, user.UID) if err != nil { log.Fatalf("error deleting user: %s\n", err) } } } // ユーザーを削除する func deleteUser(client *auth.Client, uid string) error { err := client.DeleteUser(context.Background(), uid) if err != nil { return err } log.Printf("Successfully deleted user: %s\n", uid) return nil }まとめ
Authユーザーの一括削除は開発者にとって需要が高いと思うので、この記事が誰かの力になれたらと思います。
セキュリティ的にマズい、コードの書き方に問題がある等がありましたらコメントでご教示頂けますと幸いです。
- 投稿日:2020-03-05T01:27:24+09:00
Go言語 + GVM + GoModules + GoLandを利用する際の初期設定
概要
Go言語でパッケージ管理をGo Modules、IDEでJetBrains製のGoLandを利用する際の初期設定についての自分用メモ。1年前(2019年春)から始めた構成なのでもしかしたらもう古いかもしれない。
gvmはすでにインストール済みとする。
GoModuleの有効化
Goバージョンは1.12以上ならなんでも良い(はず)。
gvm use go1.12
で Go 言語のバージョンを指定export GO111MODULE=on
で Go Modules 有効化 (.zshrcとかに書いとくと良い)git init
で git 管理対象にすることで、go mod
にモジュール名を認識させるgo mod init
で Go Modules の初期化GoLandの設定
- GoLand 2018.3.4 以上をインストール
- Preferences を開いて、Go > Go Modules(vgo) を有効化する。(下記画像の通り)
- vgo integration support | GoLand Blog のようにパッケージのインストールを行う (普通に
go get
しても良い)
- 投稿日:2020-03-05T00:33:35+09:00
Goでスプレッドシート情報を取得する
背景
こんにちは、@harhogefooです。
Google Apps Scriptを使ってスプレッドシートの情報を取得して何かやりたいことはあると思います。
ただ、GASを使うと並行処理が書けません。
なので、複数のスプレッドシートを参照して何かしたい場合、実行時処理時間がかかってしまいます。そこで、今回はGolangのgroutineを使って、スプレッドシート情報を並行に取得する方法をまとめました。
サービスアカウントを利用してスプレッドシート情報にアクセスする
前提知識を得ておくために公式ドキュメントのQuickstartを読んでおくことがおすすめです。
https://developers.google.com/sheets/api/quickstart/go
上記リンクに書いてある内容をまとめると
- Google APIsのSpreadsheet APIを有効化し、OAuthクライアントIDを発行
- このIDを利用してプログラムからスプレッドシートにアクセス
- プログラム実行時にブラウザが立ち上がりGoogleアカウントログインを求められ、ログインをすると認証情報がファイルに保存される。これ以降は認証が不要
ただ、今回は認証フローを省きたかったので
サービスアカウントを利用したスプレッドシート情報を取得する方法を採用しました。
この方法は以下にまとまっているので、ここを参考にサービスアカウントの作成からスプレッドシートへアクセスするところまでをサクッと実装できます。
https://qiita.com/bati11/items/a4cd922149dac07981bc
参考までにスプレッドシートへアクセスするためのクライアントを取得するコードを載せておきます。import "golang.org/x/oauth2/google" func getHTTPClient(credentialsJSONData []byte) (*http.Client, error) { conf, err := google.JWTConfigFromJSON(credentialsJSONData, "https://www.googleapis.com/auth/spreadsheets") if err != nil { return nil, errors.WithStack(err) } return conf.Client(oauth2.NoContext), nil }oauth2/googleのドキュメントのリンク↓
https://godoc.org/golang.org/x/oauth2/googleSpreadsheet APIを使ってスプレッドシートのセル情報を取得する
Spreadsheet APIを利用してスプレッドシート情報にアクセスする方法は3つあります。
この方法については以下にまとまっていますが、選択理由を書くためこの記事でも整理しておきます。
https://qiita.com/howdy39/items/5473160c93030c386c2d①spreadsheets.get + includeGridDataオプションを付与して取得する
スプレッドシートの全てのセル情報を含めて取得します。includeGridDataオプションを付与しないとセル情報無しで返ってきます。
Golangの具体的なコードに落とすとimport "google.golang.org/api/sheets/v4" func main() { client, _ := getHTTPClient() // ここは先述のサービスアカウント情報を使ってクライアントを取得する service, _ := sheets.New(client) resp, _ := service.Spreadsheets.Get(spreadsheetID).IncludeGridData(true).Do() // ごにょごにょ }SpreadsheetsService.Getのドキュメントは以下↓
https://godoc.org/google.golang.org/api/sheets/v4#SpreadsheetsService.Get②spreadsheets.values.get で取得する
スプレッドシート内のシートと範囲を指定して取得します。
import "google.golang.org/api/sheets/v4" func main() { client, _ := getHTTPClient(credentialsJSONData) service, _ := sheets.New(client) rangeStr := "シート名" // シート名!A1:A99 のような記述をすれば範囲の指定も可能 resp, _ := service.Spreadsheets.Values.Get(spreadsheetID, rangeStr).IncludeGridData(true).Do() // ごにょごにょ }SpreadsheetsValuesService.Getのドキュメントは以下↓
https://godoc.org/google.golang.org/api/sheets/v4#SpreadsheetsValuesService.Get③spreadsheets.values.batchGetで取得する
gRPCの構文を使ってスプレッドシートから複数のシート情報を取得することができます。
試していないのでサンプルコードはありません今回採用したセル情報取得方式
①spreadsheets.get + includeGridDataオプションを付与して取得する を採用しました。
②を採用しなかった理由
- シートごとにリクエストするため、大量に実行するとすぐにAPI制限に引っかかってしまう
- スプレッドシートの読み取りは100秒あたり500リクエストまで ### ③を採用しなかった理由
- スプレッドシート内のセル情報全てを取得したかった
- ①の方が簡潔にかけそう ## 取得したセル情報をパースする ①spreadsheets.get + includeGridDataオプションを付与して取得する 方式で情報を取得すると以下の形式で返ってきます。 https://godoc.org/google.golang.org/api/sheets/v4#Spreadsheet APIドキュメントをたどるとセル情報は、
SpreadsheetのSheetsのData配列のRowData配列のValues配列
にそれぞれ格納されており、Valuesは CellData型で定義されている(深い)。 CellDataのドキュメント↓ https://godoc.org/google.golang.org/api/sheets/v4#CellData CellDataは、FormattedValue、EffectiveValue、UserEnteredValueを持っている。 それぞれについて整理しておくと、FormattedValue: 整形された値(スプレッドシートに表示されている値)。戻り値は、string。 EffectiveValue: 評価された値。戻り値は、ExtendedValue。 UserEnteredValue: 入力された値。戻り値は、ExtendedValue。ExtendedValueのドキュメント↓
https://godoc.org/google.golang.org/api/sheets/v4#ExtendedValue
ExtendedValueは、型ごとに情報を持っています。文字列ならExtendedValue.StringValueに 数値ならExtendedValue.NumberValueに (ただし戻り値はfloat) 真偽値なら ExtendedValue.BoolValueに格納されています。セル情報の取得と表示のサンプルコード
import "google.golang.org/api/sheets/v4" func main() { client, _ := getHTTPClient() // ここは先述のサービスアカウント情報を使ってクライアントを取得する service, _ := sheets.New(client) resp, _ := service.Spreadsheets.Get(spreadsheetID).IncludeGridData(true).Do() // セル情報の取得 for _, s := range resp.Sheets { for _, row := range s.Data[0].RowData { for _, value := range row.Values { fmt.Println(value.FormattedValue) fmt.Println(value.EffectiveValue.StringValue) fmt.Println(value.UserEnteredValue.StringValue) } } } }どのValueを採用するか?
これは実際に実装をして得た知見なのですが、日付、時間データは、ExtendedValueだと妙な値が入っています(よくわからなかったので知見がありましたら教えていただきたいです)。
なので、FormattedValue(string)として扱い必要に応じて整形して読み取ります。
それ以外の数値、文字列データに関しては、EffectedValueから読み取ります。UserEnteredValueは使いませんでした。
EffectiveValueに関してはいい感じに値を取得するコードを書きました。func getValueFromEffectiveValue(ev *sheets.ExtendedValue) interface{} { if ev == nil { return "" } if ev.StringValue != "" { return ev.StringValue } if isInteger(ev.NumberValue) { return math.Floor(ev.NumberValue) } return ev.NumberValue }スプレッドシート情報を並行に取得する
さて、ここまででスプレッドシートのセル情報を取得できるようになりましたが
複数のスプレッドシートに直列アクセスをすると時間がかかるので、並行にスプレッドシートにアクセスして情報を取得します。
Golangの並行アクセス方法はgroutineやWaitGroup、errorgroupがあります。
今回はエラーハンドリングはしたいので、errorgroupを採用します。
こんな感じのコードを書きました。import ( "sync" "golang.org/x/sync/errgroup" ) func getSpreadsheetDataByID( mutex *sync.Mutex, spreadsheetID string, output *[]string, ) func() error { return func() error { // スプレッドシートからセル情報を取得する // ごにょごにょ // 並行に同一のリソースにアクセスする場合はmutexでLockをかけておくと安全 mutex.Lock() // ここで取得したセル情報をoutputに格納する mutex.Unlock() } } func main() { output := make([]string, 0) mutex := &sync.Mutex{} var eg errgroup.Group spreadSheetIDs := []string{"xxx", "yyy", "zzz"} for _, spreadSheetID := range spreadSheetIDs { // NOTE: https://golang.org/doc/faq#closures_and_goroutines spreadSheetID := spreadSheetID eg.Go(getSpreadsheetDataByID(mutex, spreadSheet, &output)) } if err := eg.Wait(); err != nil { return nil, err } // outputで何かする }errorgroupのちょっとした罠
前章でerrorgroupを使いましたが、
// NOTE
コメントの下の行を見ると
spreadsheetID := spreadsheetID
で再代入を行っています。
これを行わないと、常にリストの最後の値を参照して並行処理が走ります(期待した値が取れない)。
細かい理由についてはこちらにまとまっていますので参照してください。
https://qiita.com/harhogefoo/items/7ccb4e353a4a01cfa773終わりに
いかがでしたでしょうか。
誰かのお役に立てたら幸いです!
Happy Coding!