Google Sheets API v4 と サービスアカウントと Go でスプレッドシートを操作する

Go で書いたCLIのプログラムから Google Sheets API v4 使ってスプレッドシートを操作したときのメモ。

使用した Go のバージョンは 1.6 です。

# サービスアカウント
1. Google Developer Console でプロジェクトを作成する
2. API Manager の「ライブラリ」からGoogle Sheets API を有効にする
3. API Manager の「認証情報」からサービスアカウントを作成する。作成すると秘密鍵が書かれたJSONファイルをダウンロードできるので大事に取っておく


# スプレッドシート
1. スプレッドシートを作成する。URLからスプレッドシートIDがあるので、メモしておく。
    - `https://docs.google.com/spreadsheets/d/<スプレッドシートID>/`
2. スプレッドシートの共有設定から、サービスアカウントのメールアドレス指定して、サービスアカウントからのアクセスを許可する。


# Go からスプレッドシートにアクセス

```go
package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/sheets/v4"
)

func httpClient(credentialFilePath string) (*http.Client, error) {
	data, err := ioutil.ReadFile(credentialFilePath)
	if err != nil {
		return nil, err
	}
	conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/spreadsheets")
	if err != nil {
		return nil, err
	}

	return conf.Client(oauth2.NoContext), nil
}

func main() {
	spreadsheetId := "スプレッドシートのURLから抽出したID"
	credentialFilePath := "秘密鍵のJSONファイルのパス"

	client, err := httpClient(credentialFilePath)
	if err != nil {
		log.Fatal(err)
	}

	sheetService, err := sheets.New(client)
	if err != nil {
		log.Fatalf("Unable to retrieve Sheets Client %v", err)
	}

	_, err = sheetService.Spreadsheets.Get(spreadsheetId).Do()
	if err != nil {
		log.Fatalf("Unable to get Spreadsheets. %v", err)
	}

	fmt.Printf("success!\n")
}
```

`go run main.go` で実行。ここで403エラーが発生する場合は、スプレッドシートにサービスアカウントからのアクセスが許可されていない。


# スプレッドシートを操作
Google Sheets API v4 のリファレンスはこちら。

https://developers.google.com/sheets/reference/rest/

以下の3つに分類されている。

- spreadsheets
- spreadsheets.sheets
- spreadsheets.values

Go では以下のように対応している

- `sheetService.Spreadsheets`
- `sheetService.Spreadsheets.Sheets`
- `sheetService.Spreadsheets.Values`

Go のリファレンスはこちら。

https://godoc.org/google.golang.org/api/sheets/v4

## GET /v4/spreadsheets/{spreadsheetId}
例えば、 spreadsheets の get (GET /v4/spreadsheets/{spreadsheetId}) を使いたい場合、[リファレンス](https://developers.google.com/sheets/reference/rest/v4/spreadsheets/get) を見てみると、パスパラメータとして `spreadsheetId` が必要なことが分かる。

Go で書くとこうなる。 `sheetService.Spreadsheets.Get(spreadsheetId)` で `SpreadsheetsGetCall` を取得できるので、それの `Do()` メソッドを呼べば実際にAPIを呼ぶ。

```go
sheetService.Spreadsheets.Get(spreadsheetId).Do()
```

https://godoc.org/google.golang.org/api/sheets/v4#SpreadsheetsService.Get

## PUT /v4/spreadsheets/{spreadsheetId}/values/{range}
spreadsheets.values の update ( PUT /v4/spreadsheets/{spreadsheetId}/values/{range} ) を使いたい場合、[リファレンス](https://developers.google.com/sheets/reference/rest/v4/spreadsheets.values/update)を見ると、パスパラメータとして `spreadsheetId` と `range` が必要なことが分かる。クエリパラメータとして [valueInputOption](https://developers.google.com/sheets/reference/rest/v4/ValueInputOption) があって、リクエストボディとして [ValueRange](https://developers.google.com/sheets/reference/rest/v4/spreadsheets.values#ValueRange) が必要なことも分かる。

Go で書くとこうなる。

```go
sheetService.Spreadsheets.Values.Update(spreadsheetId, targetRange, valueRange).ValueInputOption("USER_ENTERED").Do()
```

https://godoc.org/google.golang.org/api/sheets/v4#SpreadsheetsValuesService.Update

引数の `targetRange` はただの文字列で、 `"シート名!A1:B1"` というような書式。 `valueRange` は struct でドキュメントに詳しく書いてある -> https://godoc.org/google.golang.org/api/sheets/v4#ValueRange


## 何か操作してみる

行を挿入してから、以下のように書き込んでみる。

|hoge|1   |
|----|----|
|fuga|2   |

- A2 = hoge
- B2 = 1
- A3 = fuga
- B3 = 2

行の挿入はAPIリファレンスを見ているとなさそうに思えるが、 POST /v4/spreadsheets/{spreadsheetId}:batchUpdate で色々できる。

https://developers.google.com/sheets/reference/rest/v4/spreadsheets/request

Go のコードはこんな感じ。1行目に挿入することはできなかったので、2行目から2行分挿入して、データを書き込んでいる。

```go
package main

import (
	"errors"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"golang.org/x/oauth2"
	"golang.org/x/oauth2/google"
	"google.golang.org/api/sheets/v4"
)

func httpClient(credentialFilePath string) (*http.Client, error) {
	data, err := ioutil.ReadFile(credentialFilePath)
	if err != nil {
		return nil, err
	}
	conf, err := google.JWTConfigFromJSON(data, "https://www.googleapis.com/auth/spreadsheets")
	if err != nil {
		return nil, err
	}

	return conf.Client(oauth2.NoContext), nil
}

func sheetId(s *sheets.Spreadsheet, sheetName string) (int64, error) {
	for _, sheet := range s.Sheets {
		if sheet.Properties.Title == sheetName {
			return sheet.Properties.SheetId, nil
		}
	}
	return 0, errors.New(sheetName + " is not exists.")
}

func insertRow(sheetService *sheets.Service, spreadsheetId string, sheetId int64) (*sheets.BatchUpdateSpreadsheetResponse, error) {
	req := sheets.Request{
		InsertDimension: &sheets.InsertDimensionRequest{
			InheritFromBefore: false,
			Range: &sheets.DimensionRange{
				Dimension:  "ROWS",
				StartIndex: 1,
				EndIndex:   3,
				SheetId:    sheetId,
			},
		},
	}
	insertRowReq := sheets.BatchUpdateSpreadsheetRequest{
		Requests: []*sheets.Request{&req},
	}
	return sheetService.Spreadsheets.BatchUpdate(spreadsheetId, &insertRowReq).Do()
}

func main() {
	spreadsheetId := "スプレッドシートのURLから抽出したID"
	credentialFilePath := "秘密鍵のJSONファイルのパス"
	sheetName := "シート1"

	client, err := httpClient(credentialFilePath)
	if err != nil {
		log.Fatal(err)
	}

	sheetService, err := sheets.New(client)
	if err != nil {
		log.Fatalf("Unable to retrieve Sheets Client %v", err)
	}

	spreadsheet, err := sheetService.Spreadsheets.Get(spreadsheetId).Do()
	if err != nil {
		log.Fatalf("Unable to get Spreadsheets. %v", err)
	}

	sheetId, err := sheetId(spreadsheet, sheetName)
	if err != nil {
		log.Fatal(err)
	}

	_, err = insertRow(sheetService, spreadsheetId, sheetId)
	if err != nil {
		log.Fatalf("Unable to insert row. %v", err)
	}

	valueRange := &sheets.ValueRange{
		MajorDimension: "ROWS",
		Values: [][]interface{}{
			[]interface{}{"hoge", 1},
			[]interface{}{"fuga", 2},
		},
	}
	_, err = sheetService.Spreadsheets.Values.Update(spreadsheetId, sheetName+"!A2:B3", valueRange).ValueInputOption("USER_ENTERED").Do()
	if err != nil {
		log.Fatalf("Unable to write value. %v", err)
	}

	fmt.Printf("success!\n")
}
```