20200305のGoに関する記事は8件です。

[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 さんがもっと具体的なコードを書いてくださってますので、こういう記事に興味があるかたはそちらも併せてどうぞ!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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の使い方を簡単に分かりやすくまとめてみた - Qiita

package 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なんかこわくない - Qiita

runtime

どこのファイルでかつどこのラインでエラーが出ているのかわかるようにしたいです。
そんな時は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向け) · GitHub

GCPでのfluentd

loggerには、fluentdからログが送られている。
配置は、daemonsetで全てのnodeに配置している。

fluentdを導入時にまず知っておいたほうがよさそうなこと(インストール、監視、HA構成、チューニングなど) - Qiita
GKEではStackDriver Loggingにどうやってログを送っているか - Speaker Deck
Fluentdとはどのようなソフトウェアなのか - たごもりすメモ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者レベルでも気づく、JavaとGoのswitch文の記述の違い

基本的なswitch文の記法

Java
switch rank {
    case 1:
        System.out.println("金メダル!");
        break;
    case 2:
        System.out.println("銀メダル!");
        break;
    case 3:
        System.out.println("銅メダル!");
        break;
    default:
        System.out.println("メダルを獲得できませんでした");
        break;
}
Go
switch 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

Java
switch 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;
}
Go
switch 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文の記法については、言語によって少なからぬ差が出ますね。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Golang] gorilla/muxのCORS対処法

golangでサーバーを立てるときに、ルーターとして、gorilla/muxを使っている方もいると思います。gorilla/muxでCORSの問題を解決するとき、ハマってしまったので、まとめておきます。

このサイトを参考にして以下のようなミドルウェアを挿入してみましたが、No 'Access-Control-Allow-Origin' header is present on the requested resource.
エラーが出てしまいます。

router.go
package 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についての知見が深まったのでいい経験を詰めた事にします。

参考
https://asanchez.dev/blog/cors-golang-options/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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からのストリームが受け取れていると思います!

おわり

とりあえずこれでちゃんと動くであろうストリーミングのツールたちを手に入れることができたので,楽しく開発をすすめて行こうと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Go言語】FirebaseAuthのユーザーを全て削除する

概要

FirebaseAuthを検証で使ってたら example1@example.org みたいなユーザーが大量に増えてしまったので一括で削除を行いたいが、Firebaseのコンソールからは出来ないみたいだったので Firebase Admin SDK で一括削除を行うことにした。該当記事が見つけられなかったので自分用メモも兼ねて記事を書いた。

今回は Go 言語で削除処理を書いたが、下記の言語も対応している模様。

  • Node.js
  • Java
  • Python
  • C#

参考: サーバーに Firebase Admin SDK を追加する

前準備

  1. go get firebase.google.com/go で Firebase Admin SDK を導入
  2. Firebaseコンソールからサービスアカウントキーを取得する

サービス アカウント用の秘密鍵ファイルを生成するには:

1. Firebase コンソールで、[設定] > [サービス アカウント] を開きます。
2. [新しい秘密鍵の生成] をクリックし、[キーを生成] をクリックして確定します。
3. キーを含む JSON ファイルを安全に保管します。

※ https://firebase.google.com/docs/admin/setup より引用

実装

全ユーザーを一括削除するAPIは存在しなかったので、全ユーザーを取得してイテレータで1つずつ削除処理を行った。
参考: ユーザー管理  |  Firebase

package 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ユーザーの一括削除は開発者にとって需要が高いと思うので、この記事が誰かの力になれたらと思います。
セキュリティ的にマズい、コードの書き方に問題がある等がありましたらコメントでご教示頂けますと幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Go言語 + GVM + GoModules + GoLandを利用する際の初期設定

概要

Go言語でパッケージ管理をGo Modules、IDEでJetBrains製のGoLandを利用する際の初期設定についての自分用メモ。1年前(2019年春)から始めた構成なのでもしかしたらもう古いかもしれない。

gvmはすでにインストール済みとする。

GoModuleの有効化

Goバージョンは1.12以上ならなんでも良い(はず)。

  1. gvm use go1.12 で Go 言語のバージョンを指定
  2. export GO111MODULE=on で Go Modules 有効化 (.zshrcとかに書いとくと良い)
  3. git init で git 管理対象にすることで、go mod にモジュール名を認識させる
  4. go mod init で Go Modules の初期化

GoLandの設定

  1. GoLand 2018.3.4 以上をインストール
  2. Preferences を開いて、Go > Go Modules(vgo) を有効化する。(下記画像の通り)
  3. vgo integration support | GoLand Blog のようにパッケージのインストールを行う (普通に go get しても良い)

Image from Gyazo

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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/google

Spreadsheet 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の構文を使ってスプレッドシートから複数のシート情報を取得することができます。
試していないのでサンプルコードはありません:bow:

今回採用したセル情報取得方式

①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!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む