Go で Azure Storage Blobを扱うときの覚え書き

# Go で Azure Storage Blobを扱うときの覚え書き

azblobに関する、備忘録代わりの記事です。（古い記事やパッケージなどが検索に紛れてきてしまうので）

SDKのリポジトリは以下にあります。執筆時点で v1.0.0 が最新です(ちょうど執筆前にリリースされたようです） 以前のバージョン（v0.5 とか v0.6）を使っている人はアップデートするとよいでしょう。

- https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/storage/azblob#readme
- https://github.com/Azure/azure-sdk-for-go/releases/tag/sdk%2Fstorage%2Fazblob%2Fv1.0.0

パッケージは以下にあります。
- https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/storage/azblob

`azblob`と検索すると、古いバージョンが先にヒットしてしまう場合がありあますが、理由がなければ使わないようにしましょう。
- https://github.com/Azure/azure-storage-blob-go

## 一般的な使い方

他のSDKでよくある使い方、例えば

1. 接続文字列でクライアントを作る
1. コンテナオブジェクトを作り、コンテナを作成する（CreateIfNotExistとか）
1. BLOBオブジェクトを作り、アップロード・ダウンロードする

みたいなシナリオをGoで書いてみます。

## 初期設定

初期化してSDKを取得しておきます。

```
go mod init main
go get github.com/Azure/azure-sdk-for-go/sdk/storage/azblob
```

接続文字列外のクレデンシャルを使う場合は、azidentity も必要です。

```
go get github.com/Azure/azure-sdk-for-go/sdk/azidentity
```

## 接続からコンテナの作成まで

接続文字列からクライアントを作成して、コンテナを作る簡単なコード例です。`AZURE_STORAGE_CONNECTION_STRING` に接続文字列を設定しておきます。
特に難しいところないのですが、コンテナが既に存在するとエラーが発生するので、自分でハンドルするしか無さそうです。どうやってハンドルするのか分からなかったので、ソースをつらつら参照していたら、`bloberror.HasCode` という関数があったので使ってみましたが、一応これで判断が可能ですが、正しい方法か分かりません。

パッケージの説明には、

| CreateContainer is a lifecycle method to creates a new container under the specified account. If the container with the same name already exists, a ResourceExistsError will be raised. This method returns a client with which to interact with the newly created container.

と、`ResourceExistsError` なるものがRaiseされると書かれていましたが、ソースをググってもそれに該当しそうなコードは見つかりませんでした。なんとなく他の言語のSDKの説明なのではないかという感じです（Pythonぽい）。

```go
	ctx := context.Background()
	connectionString, ok := os.LookupEnv("AZURE_STORAGE_CONNECTION_STRING")
	if !ok {
		log.Fatal("'AZURE_STORAGE_CONNECTION_STRING' not found")
	}

	// 接続文字列でクライアントを作成する
	serviceClient, err := azblob.NewClientFromConnectionString(connectionString, nil)
	if err != nil {
		panic(err)
	}

    // コンテナの作成
	r, err := serviceClient.CreateContainer(ctx, "test-container", &azblob.CreateContainerOptions{})

	if err != nil {
		if bloberror.HasCode(err, bloberror.ContainerAlreadyExists) {
			// コンテナが既に存在する
			fmt.Printf("container already exists")
		} else {
			panic(err)
		}
	}
	fmt.Printf("%v\n", r)

```

## ファイルのアップロード

他のSDKだとコンテナを作成すると、コンテナ用のクライアントオブジェクトが取得できて、それに対してBlobClientを作成してアップロードみたいな感じですが、Goではすべて`serviceClient` で処理します

アップロードする関数はいくつかありますが、以下はFileからの例です。特にオプションを指定しないと、Blobは常に上書きされます。

```go
	f, err := os.Open("./sample.txt")
	if err != nil {
		panic(err)
	}

	r, err := serviceClient.UploadFile(ctx, "test-container", "sample.txt", f, &azblob.UploadFileOptions{})
	if err != nil {
		panic(err)
	}
    fmt.Printf("%v\n", r2)
```

とはいえ、上書きされたくないときもあると思いますが、他のSDKのように `overwrite=false` 的な物はなくて、ETAGで制御しないとならないようです。`If-None-Match` に `*`を指定すると、リソースが存在しない限り実行される振る舞いをするので、それを利用します。


```go
	o := &azblob.UploadFileOptions{
		AccessConditions: &blob.AccessConditions{
			ModifiedAccessConditions: &blob.ModifiedAccessConditions{IfNoneMatch: to.Ptr(azcore.ETagAny)},
		},
	}
	r, err := serviceClient.UploadFile(ctx, "test-container", "sample.txt", f, o)
	if err != nil {
		panic(err)
	}
	fmt.Printf("%v\n", r2)
```

その値の細かい話は以下に書かれています。

[BLOB サービス操作の条件付きヘッダーの指定 (REST API) - Azure Storage | Microsoft Learn](https://learn.microsoft.com/ja-jp/rest/api/storageservices/specifying-conditional-headers-for-blob-service-operations)

## ダウンロード

ダウンロードにもいくつか関数があり、File/Stream/Bufferなどがあります。以下は、Streamの例です。レスポンスボディを文字に変換して出力しています。

```go
	dr, err := serviceClient.DownloadStream(ctx, "test-container", "sample.txt", nil)
	if err != nil {
		panic(err)
	}

	data, err := io.ReadAll(dr.Body)
	if err != nil {
		panic(err)
	}

	fmt.Println(string(data))
```

## まとめ

さくっと接続、アップロード、ダウンロードを試してみましたが、これがGoらしいのかはちょっとよく分かりませんが、多分そうなのでしょう。あと、他のSDKに慣れているとちょっととっつきにくいかもしれません。

一応ガイドライン的な物もあるので、紹介しておきます(以前ドラフトですけど）

[Go Azure SDK Design Guidelines | Azure SDKs](https://azure.github.io/azure-sdk/golang_introduction.html)

以上（つづく）