ソースコードから理解する技術-UnderSourceCode

手を動かす(プログラムを組む)ことで技術を理解するブログ

Effective Java 第2版 項目別一文まとめ

仕事でしばらくガッツリをJavaを触ることになりそうなので
Effective Javaを再読してみました。

Effective Java 第2版 - 丸善出版 理工・医学・人文社会科学の専門書出版社

ちなみに最新の第3版ではなく、手元にあった第2版です。

こちらのEffective Javaですが

  • 各項目のタイトルがやるべきことを明示している
  • ほとんどの項目の最後にまとめがあり、そこでもより詳細なまとめが記述されている

という構成となっています。

なので読み方(使い方)としては

  • まずは一通り通読する
  • 必要に応じて項目から検索して参照する

という形になるかと思います。(もちろん精読して頭に入っている状態が一番いいでしょうが・・・)

まだ全部は読み終えていませんが、読み終わった項目については備忘録として一文で纏めてみました。
無理やり一文で纏めていることもあり、詳細は本書を参照してください。

第2章 オブジェクトの生成と消滅

  • 項目1 コントラクタの変わりにstaticファクトリーメソッドを検討する
  • 項目2 数多くのコンストラクタパラメータに直面した時にはビルダーを検討する

  コンストラクタの変わりのメソッドを作るパターンについて。

  • 項目3 privateのコンストラクタかenum型でシングルトン特性を強制する
  • 項目4 privateのコンストラクタでインスタンス化不可能を強制する

  コンストラクタを隠蔽するパターンについて。

  • 項目5 不必要なオブジェクトの生成を避ける

  staticなオブジェクトを毎回生成しないことや、プリミティブ型を使う話。

  • 項目6 廃れたオブジェクト参照を取り除く

  使用が終了したインスタンス変数にnullを代入して参照を消す例。

  • 項目7 ファイナライザを避ける

  ファイナライザの使用を避けるべき理由と、使うべき場合。

第3章 すべてのオブジェクトに共通のメソッド

  • 項目8 equalsをオーバーライドする時は一般契約に従う
  • 項目9 equalsをオーバーライドする時は、常にhashCodeをオーバーライドする

  equalsをオーバーライドをしない or するべき場合、equals・hashCodeを
  実装するルール。

  • 項目10 toStringを常にオーバーライドする

  toStringの実装について。

  • 項目11 cloneを注意してオーバーライドする

  Cloneインターフェースの注意点、Cloneを実装すべき場合 or
  するべきでない場合。

  • 項目12 Comparableの実装を検討する

  順序をもつ場合にはComparableとするべきということ

第4章 クラスとインタフェース

  • 項目13 クラスとメンバーへのアクセス可能性を最小限にする
  • 項目14 publicのクラスでは、publicのフィールドではなく、アクセッサーメソッドを使う

  フィールドの公開範囲は狭めること、private classのフィールドのみは
  publicにしてもいいこと。

  • 項目15 可変性を最小限にする

  不変オブジェクトの実装例、可変にする理由がない場合は
  クラス自体を不変クラスに・フィールドもfinalをつけて不変にするべきということ。

  一人のプログラマが管理できるような一パッケージ内の継承は安全だが、
  別パッケージの実装クラスを継承するのは危険だということ。

  • 項目17 継承のために設計および文書化する、でなければ継承を禁止する

  継承にまつわるオーバーライドの危険性、及びそれを避けるために継承を禁止すること。

  • 項目18 抽象クラスよりインタフェースを選ぶ

  複数の実装を許す型を定義するのにインターフェースは良い方法、
  骨格実装を同時に公開することで利用を助けることができる。

  • 項目19 型を定義するためだけにインタフェースを使用する

  定数の定義のためだけにインターフェースは使用すべきではないということ。

  • 項目20 タグ付クラスよりクラス階層を選ぶ

  複数の性質をタグで分けているクラスを、適切な個々のクラスに分割する例。

  • 項目21 戦略を表現するために関数オブジェクトを使用する

  Javaの関数オブジェクトの説明、使用するパターンについて。

  • 項目22 非staticのメンバークラスよりstaticのメンバークラスを選ぶ

  4種類のクラス内にネストしたメンバークラスと、それぞれの使用すべき場合について。

第5章 ジェネリック

  • 項目23 新たなコードで原型を使用しない

  ジェネリクスで原型を使用すると実行時にエラーとなるため
  すべきではないということ。

  • 項目24 無検査警告を取り除く

  無検査警告を取り除くこと、@SuppressWarningを使って警告を取り除く方法。

  • 項目25 配列よりリストを選ぶ

  代入する型が違う場合、配列は実行時にエラーを、
  リストはコンパイル時にエラーを吐く。

  型、メソッドでのジェネリック化の実例。

  ワイルドカード型を使用することでAPIの利用を柔軟にする。

  • 項目29 型安全な異種コンテナーを検討する

  異種の型を安全に保持するコンテナーの実例。

第6章 enumアノテーション

  • 項目30 int定数の代わりにenumを使用する

  Javaenumはクラスであり、振る舞いを持つことができる。

  enumの宣言時に具体的な値も設定すること。

  • 項目32 ビットフィールドの代わりにEnumSetを使用する

  EnumSetの使用例。

  • 項目33 序数インデックスの代わりにEnumMapを使用する

  配列をインデックスするためにはEnumMapを使うこと。

  • 項目34 拡張可能なenumをインタフェースで模倣する

  インターフェースの実装としてenumを書く例。

  アノテーションの定義の例。

  意図しないバグを防ぐために@Overrideをつけるべきということ。

  • 項目37 型を定義するためにマーカーインタフェースを使用する

  マーカーインターフェースとマーカーアノテーションの使用について。

第7章メソッド

  • 項目38 パラメータの正当性を検査する

  publicのメソッドやコンストラクタではパラメータのチェックを行うこと。

  • 項目39 必要な場合には、防御的にコピーする

  オブジェクトを受け取る or 返却するメソッドを持つクラスで、不変性を維持する例。

  良いメソッドを書くために留意すべき幾つかの項目。

  オーバーロードで実行されるメソッドの決定がコンパイル時に
  行われることによる「罠」と、オーバーロードをあまり使うべきでは
  無いということ。

  • 項目42 可変長引数を注意して使用する

  可変長引数の使い方の実例。

  • 項目43 nullではなく、空配列か空コレクションを返す

  呼び出し元でのnullチェックのことも考慮し、空配列か空コレクションを返す。

  • 項目44 すべての公開API要素に対してドキュメントコメントを書く

  ドキュメントの必要性、実例とJavaDocについて。(現在だとswagger?)

第8章 プログラミング一般

  • 項目45 ローカル変数のスコープを最小限にする

  変数のスコープを最小化すべきということ。

  • 項目46 従来のfor ループよりfor-each ループを選ぶ

  Java8以降はラムダを使う?

  • 項目47 ライブラリーを知り、ライブラリーを使う

  標準ライブラリ、特にjava.lang、java.util、java.ioをよく知るべきということ。

  • 項目48 正確な答えが必要ならば、float とdouble を避ける

  金額など正確な値を求める時にはBigDecimal、int、longなどを使うこと。

  • 項目49 ボクシングされた基本データより基本データ型を選ぶ

  オブジェクト型のバグの例と、アンボクシングを使ってプリミティブ型を使う例。

  • 項目50 他の型が適切な場所では、文字列を避ける

  キーのように意味のある値には適切な型を作成すること。

  • 項目51 文字列結合のパフォーマンスに用心する

  文字列連結にはStringBuilderを使うこと。

  • 項目52 インタフェースでオブジェクトを参照する

  実装を切り替える事を考慮してインターフェースを使うべきということ。

  • 項目53 リフレクションよりインタフェースを選ぶ

  コンパイル時に分かるインターフェースや親クラスを使うべきということ。

  • 項目54 ネイティブメソッドを注意して使用する

  ネイティブメソッドは滅多に使わない、使う場合は「徹底的にテスト」すること。

  • 項目55 注意して最適化する

  速さよりより良い設計のプログラムを書き、最後にパフォーマンスチューニングを
  行うということ。

  • 項目56 一般的に受け入れられている命名規約を守る

  一般的な命名規則に従う重要性について。

第9章 例外233

  • 項目57 例外的状態にだけ例外を使用する
  • 項目58 回復可能な状態にはチェックされる例外を、プログラミングエラーには実行時例外を使用する
  • 項目59 チェックされる例外を不必要に使用するのを避ける
  • 項目60 標準例外を使用する

  検査例外、実行時例外、エラーの使い方の原則。

  • 項目61 抽象概念に適した例外をスローする

  上位レイヤーで行う例外翻訳について。

  • 項目62 各メソッドがスローするすべての例外を文書化する

  例外の文章化について。

  • 項目63 詳細メッセージにエラー記録情報を含める

  例外のメッセージについて。

  • 項目64 エラーアトミック性に努める

  再実行時のいわゆる冪統性を担保すべきということ。

  • 項目65 例外を無視しない

  例外を握り潰さざるおえない時にも、最低限の例外をログなどに記録すること。

gRPCの入門とAPIバージョニングについて

はじめに

gRPCでmicroserviceを作る以下のチュートリアルを写経してみました。
[Tutorial, Part 1] How to develop Go gRPC microservice with HTTP/REST endpoint, middleware…

この中でAPIのバージョニングを行うために、バージョンv1のフォルダを作っている処理がありました。
今回はバージョンを追加することができるかを確かめるため、v2フォルダを作成し、新しいバージョンのAPIを作成してみました。
以下、その時の手順になります。

前提条件

先のチュートリアルの一連の処理は実装してあるものとします。
またデータの格納先としてMySQLを使っているのですが、私はローカルのMySQLを使用しました。

バージョニングの手順

ではAPIの新たなバージョン「v2」を作成する手順です。

api/proto/v2/todo-service.proto に定義を追加。

api/proto/v2フォルダを作成し、v2のAPI用のtodo-service.protoファイルを作成します。
今回は全てのResponseの定義に「message」を追加しました。

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0#diff-6bf81ca75e48c17bfe1d6e7cf5b6de84

./third_party/protoc-gen.sh を実行して pkg/api/v2/todo-service.pb.go を作成

参照したチュートリアルではprotocコマンドの実行に、.shファイルを使用します。
./third_party/protoc-gen.shにv2のprotocコマンドを実行するよう追加します。

protoc --proto_path=api/proto/v2 --proto_path=third_party --go_out=plugins=grpc:pkg/api/v2 todo-service.proto

追加したら./third_party/protoc-gen.shを実行します。
実行するとv2用のpkg/api/v2/todo-service.pb.goが作成されます。

pkg/cmd/server.go、pkg/protocol/grpc/server.goにv2を追加。

サーバを起動するpkg/cmd/server.go、pkg/protocol/grpc/server.goに、v2のAPIをそれぞれ追加します。

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0#diff-a4874ae26fb75dd53cb22801a2441507

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0#diff-2b80d164c22689ccea25a0527d415c17

pkg/service/v2/todo-service.go に独自ロジックを追加

サーバ側の処理を記述するpkg/service/にv2フォルダを作成し、その中にtodo-service.goを作成します。
先にも書いたようにサーバ側の処理をこの中で実装します。
今回はv2独自のロジックとして、各APIのResponseにメッセージとして「v2API Message」を返却するようにしました。

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0#diff-bf95c4339b6a85e3c3d682e5e4bae737

cmd/client-grpc-v2/main.go にクライアント側の処理を追加

クライアント側からはv2のAPIを呼び出します。
今回は既存のv1とは別に新しいクライアントを作成しました。
cmd内にclient-grpc-v2フォルダを作成し、main.goを作成します。
処理自体はv1とほぼ同じで、v1の代わりにv2を呼び出しているだけです。

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0#diff-6780540a19e75fbafe09b11e580c5fdb

実行してみる

サーバをビルドし、起動します。
cmd/serverに移動してgo buildを行います。
起動は以下のようなコマンドとなります。

./server -grpc-port=9090 -db-host=localhost:3306 -db-user=YourUser -db-password=YourPassword -db-schema=YourSchema

v1、v2を呼び出すクライアントをそれぞれビルドします。
cmd/client-grpc、cmd/client-grpc-v2に移動してgo buildを行います。

まずはv1を実行してみます。

$ ./client-grpc -server=localhost:9090
2018/09/26 16:37:16 Create result: <api:"v1" id:2 >

2018/09/26 16:37:16 Read result: <api:"v1" toDo:<id:2 title:"title (2018-09-26T07:37:16.476193Z)" description:"description (2018-09-26T07:37:16.476193Z)" reminder:<seconds:1537947436 > > >

2018/09/26 16:37:16 Update result: <api:"v1" updated:1 >

2018/09/26 16:37:16 ReadAll result: <api:"v1" toDos:<id:2 title:"title (2018-09-26T07:37:16.476193Z)" description:"description (2018-09-26T07:37:16.476193Z) + updated" reminder:<seconds:1537947436 > > >

2018/09/26 16:37:16 Delete result: <api:"v1" deleted:1 >

続いてv2を実行してみます。

$ ./client-grpc-v2 -server=localhost:9090
2018/09/26 16:46:29 Create result: <api:"v2" id:1 message:"v2API Message" >

2018/09/26 16:46:29 Read result: <api:"v2" toDo:<id:1 title:"title (2018-09-26T07:46:29.05597Z)" description:"description (2018-09-26T07:46:29.05597Z)" reminder:<seconds:1537947989 > > message:"v2API Message" >

2018/09/26 16:46:29 Update result: <api:"v2" updated:1 message:"v2API Message" >

2018/09/26 16:46:29 ReadAll result: <api:"v2" toDos:<id:1 title:"title (2018-09-26T07:46:29.05597Z)" description:"description (2018-09-26T07:46:29.05597Z) + updated" reminder:<seconds:1537947989 > > message:"v2API Message" >

2018/09/26 16:46:29 Delete result: <api:"v2" deleted:1 message:"v2API Message" >

v2の方のResponseには、追加した「message:"v2API Message"」が表示されているのが分かるかと思います。

まとめ

このようにgRPCの複数のAPIのバージョンを共存させることができました。
バージョンを追加した際のソース差分は以下となります。

https://github.com/SrcHndWng/go-learning-gRPC-microservice/compare/v1.0.0...v1.1.0

また全部のソースは以下となります。
GitHub - SrcHndWng/go-learning-gRPC-microservice at v1.1.0

Firestoreにコレクションをネストしたデータを登録する

はじめに

前回に引き続き、Firestoreについてです。
今回は公式ドキュメントの「サブコレクション」を参考に、コレクションをネストして登録し、参照してみました。

https://firebase.google.com/docs/firestore/data-model?authuser=0

登録するデータの構成は以下のようになります。

「rooms」コレクション
    + 「roomA」ドキュメント
        + 「messages」コレクション
            + 「message1」ドキュメント
                - From: "alex"
		        - Msg:  "Hello World!"
            + 「message2」ドキュメント
                - From: "tom"
		        - Msg:  "Hello Tom!"
    + 「roomB」ドキュメント
        + 「messages」コレクション
            + 「message3」ドキュメント
                - From: "ken"
		        - Msg:  "Hello Ken!"

サンプルプログラム

今回実装したプログラムは以下になります。

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"google.golang.org/api/iterator"

	"cloud.google.com/go/firestore"
	firebase "firebase.google.com/go"
	"google.golang.org/api/option"
)

type Room struct {
	Name string `firestore:"name,omitempty"`
}

type Message struct {
	From string `firestore:"from,omitempty"`
	Msg  string `firestore:"msg,omitempty"`
}

func main() {
	fmt.Println("----- main start -----")

	jsonPath := os.Getenv("FIREBASE_JSON_PATH")

	opt := option.WithCredentialsFile(jsonPath)
	ctx := context.Background()
	app, err := firebase.NewApp(context.Background(), nil, opt)
	if err != nil {
		log.Fatalln(err)
	}

	client, err := app.Firestore(ctx)
	if err != nil {
		log.Fatalln(err)
	}
	defer client.Close()

	err = create(ctx, client)
	if err != nil {
		log.Fatalln(err)
	}

	err = reference(ctx, client)
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Println("----- main end -----")
}

func reference(ctx context.Context, client *firestore.Client) error {
	docs := client.Collection("rooms").Documents(ctx)
	for {
		doc, err := docs.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			return err
		}
		fmt.Printf("document id = %s\n", doc.Ref.ID)
		msgDocs := doc.Ref.Collection("messages").Documents(ctx)
		for {
			msgDoc, err := msgDocs.Next()
			if err == iterator.Done {
				break
			}
			if err != nil {
				return err
			}
			fmt.Println(msgDoc.Data())
		}

	}

	return nil
}

func create(ctx context.Context, client *firestore.Client) error {
	roomA := Room{
		Name: "my chat room",
	}
	message1 := Message{
		From: "alex",
		Msg:  "Hello World!",
	}
	_, err := client.Collection("rooms").Doc("roomA").Set(ctx, roomA)
	if err != nil {
		return err
	}
	_, err = client.Collection("rooms").Doc("roomA").Collection("messages").Doc("message1").Set(ctx, message1)
	if err != nil {
		return err
	}

	message2 := Message{
		From: "tom",
		Msg:  "Hello Tom!",
	}
	_, err = client.Collection("rooms").Doc("roomA").Collection("messages").Doc("message2").Set(ctx, message2)
	if err != nil {
		return err
	}

	roomB := Room{
		Name: "beta room",
	}
	message3 := Message{
		From: "ken",
		Msg:  "Hello Ken!",
	}
	_, err = client.Collection("rooms").Doc("roomB").Set(ctx, roomB)
	if err != nil {
		return err
	}
	_, err = client.Collection("rooms").Doc("roomB").Collection("messages").Doc("message3").Set(ctx, message3)
	if err != nil {
		return err
	}

	return nil
}

前回と同様に、秘密鍵jsonのパスを環境変数「FIREBASE_JSON_PATH」に保持しました。
create()で登録を行い、reference()で参照を行なっています。

create()では上記のデータ構成に従ってデータを登録しています。
reference()では固定値として「rooms」「messages」以外はIDを動的に取得して参照するようにしています。

作成したプログラムは以下にも登録しました。
GitHub - SrcHndWng/go-learning-firestore-samples

FirestoreにGolangでデータを登録してみる

はじめに

GoogleのFirebaseを初めて触ってみました。
今回はFirestoreにGolangでデータを登録・参照してみました。
以下、その時のメモと、作ったソースです。

手順について

まずはGoogleのアカウントが必要なので、あらかじめ作っておく必要があります。
基本的な手順は、以下の公式ドキュメントの通りとなります。

https://firebase.google.com/docs/firestore/quickstart?authuser=0

1. プロジェクト、データベースの作成

上記の公式ドキュメントのリンクからFirebaseコンソールを開き、プロジェクトを作成します。
次にデータベースを作りますが、この時にルールは厳しい方(名前はメモするのを忘れた)を選択します。
(外部からアクセスできない設定にします。後で変更します。)

2. データベースのルール変更

先の設定ではプログラムからはアクセスできないので変更します。
今回はユーザIDが正しい場合のみアクセスできるようにしました。
Firebaseコンソールの「ルール」タブを開き、権限設定を以下のように変更します。

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      match /users/{userId} {
        allow read, write: if request.auth.uid == userId;
      }
    }
  }
}

「allow read, write: if request.auth.uid == userId;」のifの右で
read・writeを許可する条件としてユーザIDが一致することを定義しています。

3. SDKの取得

FirebaseのSDKがない場合、go getで取得する必要があります。
この時、-uオプションをつけないで実行すると以下のようなエラーとなりました。

# go.opencensus.io/plugin/ocgrpc
../../../go.opencensus.io/plugin/ocgrpc/trace_common.go:43:9: undefined: metadata.AppendToOutgoingContext

これを回避するには-uオプションをつけ、以下のようにgo getを実行しました。

go get -u firebase.google.com/go

4. サービスアカウントの秘密鍵を取得

サービスアカウントの秘密鍵を取得します。
取得する場所が分かりにくかったのですが、プロジェクトに紐づいたアカウントを取得するため、以下の箇所から取得しました。

  • 画面左上の「Project Overview」から「プロジェクトの設定」を選択
  • 「サービス アカウント」タグを選択
  • 「新しい秘密鍵の生成」ボタンを押下

秘密鍵の形式はjsonを選択し、ローカルに保存して共有しないようにします。
またここで各プログラム言語ごとにFirebaseアプリケーションのオブジェクトを作るサンプルソースが表示されるので、これもメモっておきます。

5. プログラムの作成

上記でメモったサンプルソースと、公式サイトのソースを元に、今回は以下のようなプログラムを作成しました。

package main

import (
	"fmt"
	"log"
	"os"

	"golang.org/x/net/context"

	firebase "firebase.google.com/go"

	"google.golang.org/api/iterator"
	"google.golang.org/api/option"
)

func main() {
	fmt.Println("----- main start -----")

	jsonPath := os.Getenv("FIREBASE_JSON_PATH")

	opt := option.WithCredentialsFile(jsonPath)
	ctx := context.Background()
	app, err := firebase.NewApp(context.Background(), nil, opt)
	if err != nil {
		log.Fatalln(err)
	}

	client, err := app.Firestore(ctx)
	if err != nil {
		log.Fatalln(err)
	}
	defer client.Close()

	// create
	_, _, err = client.Collection("users").Add(ctx, map[string]interface{}{
		"first": "Ada",
		"last":  "Lovelace",
		"born":  1815,
	})
	if err != nil {
		log.Fatalf("Failed adding alovelace: %v", err)
	}

	// create
	_, _, err = client.Collection("users").Add(ctx, map[string]interface{}{
		"first":  "Alan",
		"middle": "Mathison",
		"last":   "Turing",
		"born":   1912,
	})
	if err != nil {
		log.Fatalf("Failed adding aturing: %v", err)
	}

	// reference
	iter := client.Collection("users").Documents(ctx)
	for {
		doc, err := iter.Next()
		if err == iterator.Done {
			break
		}
		if err != nil {
			log.Fatalf("Failed to iterate: %v", err)
		}
		fmt.Println(doc.Data())
	}

	fmt.Println("----- main end -----")
}

秘密鍵jsonのパスを環境変数「FIREBASE_JSON_PATH」として保持した以外は、各サンプルソースそのままとなっています。
(一部のコメント、fmt.Printlnを除く。)

これを実行すると以下のようにターミナルに出力されます。

----- main start -----
map[last:Turing first:Alan born:1912 middle:Mathison]
map[first:Ada born:1815 last:Lovelace]
----- main end -----

Firebaseコンソールのデータベースを確認しても、プログラムにて登録したデータが表示されるはずです。

最後に、今回作成したソースは以下になります。
GitHub - SrcHndWng/go-learning-firestore

BoltでGoroutineを使って登録する場合のDB.Batch()について

はじめに

昨日に引き続きにBoltついてです。
GitHub - boltdb/bolt: An embedded key/value database for Go.

今回はGoroutineの中でBoltに登録処理を行うケースを想定し、DB.Batch()を検証してみました。

DB.Batch()

公式のドキュメントは以下となります。
https://github.com/boltdb/bolt#batch-read-write-transactions

DB.Batch()を使うと、DB.Update()が一件毎にディスクに書き込むのに対して、まとめて書き込むことでオーバーヘッドを無くすことができるようです。
ただし注意点もあり、複数あるGoroutineの中でエラーが発生しても、別のGoroutineは実行されてしまいます。

サンプルソース

DB.Batch()を使ったサンプルソースを書いてみました。
特に長くはないので全行載せます。

package main

import (
	"fmt"
	"log"
	"sync"

	"github.com/boltdb/bolt"
)

const dbFile = "sample.db"
const bucket = "todos"

func main() {
	fmt.Println("main start...")

	db, err := bolt.Open(dbFile, 0600, nil)
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	batch(db)
	reference(db)

	fmt.Println("main end...")
}

func batch(db *bolt.DB) {
	var wg sync.WaitGroup
	ids := []string{"1", "2", "3", "4", "5"}

	for _, id := range ids {
		wg.Add(1)
		go func(itemID string) {
			fmt.Printf("goroutine called. itemId = %s\n", itemID)
			err := db.Batch(func(tx *bolt.Tx) error {
				b, err := tx.CreateBucketIfNotExists([]byte(bucket))
				if err != nil {
					return err
				}
				// comment to raise error
				// if itemID == "3" {
				// 	return errors.New(fmt.Sprintf("Error! itemId = %s", itemID))
				// }
				return b.Put([]byte(itemID), []byte(fmt.Sprintf("todo %s", itemID)))
			})
			if err != nil {
				fmt.Printf("Batch() error : %v\n", err)
			}
			wg.Done()
		}(id)
	}

	wg.Wait()
}

func reference(db *bolt.DB) error {
	return db.View(func(tx *bolt.Tx) error {
		b := tx.Bucket([]byte(bucket))
		c := b.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			fmt.Printf("key:%v, value:%s\n", k, string(v))
		}
		return nil
	})
}

batch()が今回取り扱うDB.Batch()で登録処理を行なっている関数です。id 1〜5までをループし、Goroutineにて登録しています。
登録した後、reference()で参照して出力しています。

上記の状態で実行すると以下のように出力されます。

main start...
goroutine called. itemId = 5
goroutine called. itemId = 2
goroutine called. itemId = 1
goroutine called. itemId = 3
goroutine called. itemId = 4
key:[49], value:todo 1
key:[50], value:todo 2
key:[51], value:todo 3
key:[52], value:todo 4
key:[53], value:todo 5
main end...

次にbatch()内のエラーを起こす以下のコメントアウトを外して実行してみます。

// comment to raise error
if itemID == "3" {
 	return errors.New(fmt.Sprintf("Error! itemId = %s", itemID))
}

idが3の場合のみエラーを起こすようにしてみました。
この状態で実行すると、以下のようになります。

main start...
goroutine called. itemId = 5
goroutine called. itemId = 2
goroutine called. itemId = 3
goroutine called. itemId = 4
goroutine called. itemId = 1
Batch() error : Error! itemId = 3
key:[49], value:todo 1
key:[50], value:todo 2
key:[52], value:todo 4
key:[53], value:todo 5
main end...

idが3の場合にエラーが発生ましたが、他のGoutineは実行されて1・2・4・5は登録されたことが分かるかと思います。

今回私が作ったサンプルは以下になります。
GitHub - SrcHndWng/go-learning-boltdb-batch

Golangの組み込みDBとして使えるBoltについて

はじめに

前回にも書きましたが、Golangの組み込みDBとして使うことができるBoltを見つけたので、使ってみました。
GitHub - boltdb/bolt: An embedded key/value database for Go.

以下、このDBについて紹介していきたいと思います。

Boltとは

BoltはKeyValue型のデータベースで、これ自体がGolangで実装されています。
シンプルで早いDBを目指しており、またAPIやファイルフォーマットは既に安定した(stable)な状態のようです。
Golangの組み込みDBとして使うには良い選択肢ではないでしょうか。。。

Boltの特徴

Boltは以下の特徴を持っています。

1. Golangで実装されていて組み込みDBとして使える
2. データ自体が別ファイルとして保存される
3. データの集合をBucketという単位で扱う

Boltはデータの集合を「Bucket」という単位で扱います。
このBucketの中にKeyValue形式でデータを登録します。
データの集合という意味では、BucketRDBなどのテーブルに近いですが、Bucketはネストさせることができます。
これらについては、サンプルプログラムにてより詳しく見てみます。

4. トランザクションはスレッドセーフではない

これは注意点だと思います。
READMEには以下のようにあります。

Individual transactions and all objects created from them (e.g. buckets, keys) are not thread safe. To work with data in multiple goroutines you must start a transaction for each one or use locking to ensure only one goroutine accesses a transaction at a time.

GitHub - boltdb/bolt: An embedded key/value database for Go.

goroutineなどから使うときには同時にアクセスが行われないよう注意する必要があります。

サンプルプログラム

Boltを使ってサンプルプログラムを書いてみました。
一つのBucketに登録・参照・削除するプログラムと、Bucketをネストさせるプログラムです。

一つのBucketに登録・参照・削除するプログラム

まずは一つのBucketに登録・参照・削除するプログラムです。
以下のようになります。

登録・更新
db, err := bolt.Open(dbFile, 0600, nil)
(中略)

return db.Update(func(tx *bolt.Tx) error {
    b, err := tx.CreateBucketIfNotExists([]byte(bucket))
    (中略)
    err = b.Put(keybytes(id), []byte(data))
参照
db, err := bolt.Open(dbFile, 0600, nil)
(中略)

db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(bucket))
    c := b.Cursor()
    for k, v := c.First(); k != nil; k, v = c.Next() {
        (何らかの処理)
    }
削除
db, err := bolt.Open(dbFile, 0600, nil)
(中略)

db.Update(func(tx *bolt.Tx) error {
  b := tx.Bucket([]byte(bucket))
  err := b.Delete(key)
  return err
})

先にも書いたようにBoltは「Bucket」の中にデータを格納します。
なので処理の流れは

1. DBを開く(このときデータファイルが存在しなければ作成される)
2. Bucketを作る or 参照する
3. Bucketに対して登録・参照・削除する

という形となります。

Bucketをネストさせるプログラム

次にBucketをネストさせるプログラムです。
こちらも登録・参照を行います。

登録

db, err := bolt.Open(dbFile, 0600, nil)
(中略)

err = insert(db)
(中略)

func insert(db *bolt.DB) error {
	return db.Update(func(tx *bolt.Tx) error {
		// create root bucket.
		users, err := tx.CreateBucketIfNotExists([]byte(rootBucket))
		if err != nil {
			return err
		}

		// create nested bucket.
		tom, err := users.CreateBucketIfNotExists([]byte(tomBucket))
		if err != nil {
			return err
		}

		// insert.
		err = tom.Put([]byte("key1"), []byte("tom's todo"))
		if err != nil {
			return err
		}

		// create nested bucket.
		ken, err := users.CreateBucketIfNotExists([]byte(kenBucket))
		if err != nil {
			return err
		}

		// insert.
		err = ken.Put([]byte("key1"), []byte("ken's todo1"))
		if err != nil {
			return err
		}
		err = ken.Put([]byte("key2"), []byte("ken's todo2"))
		if err != nil {
			return err
		}

		return nil
	})
}

参照

db, err := bolt.Open(dbFile, 0600, nil)
(中略)

err = reference(db)
(中略)

func reference(db *bolt.DB) error {
	return db.View(func(tx *bolt.Tx) error {
		users := tx.Bucket([]byte(rootBucket))

		// select from nested bucket.
		tom := users.Bucket([]byte(tomBucket))
		c := tom.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			fmt.Printf("key:%v, value:%s\n", k, string(v))
		}

		// select from nested bucket.
		ken := users.Bucket([]byte(kenBucket))
		c = ken.Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			fmt.Printf("key:%v, value:%s\n", k, string(v))
		}

		return nil
	})
}

処理順としては

1. DBを開く
2. RootとなるBucketを作る or 参照する
3. ネストしたBucketを作る or 参照する
4. ネストしたBucketに登録 or 削除を行う

となります。

まとめ

このような感じでBoltを使ってデータを登録・削除・参照することができました。
公式のREADMEも充実しているので、使うときには一読することをお勧めします。

今回私が作ったサンプルプログラムは以下となります。
GitHub - SrcHndWng/go-learning-boltdb-todo
GitHub - SrcHndWng/go-learning-boltdb-bucket-nest

GolangのローカルDBとしてBoltを使ってみた

前回の記事でBitcoinについて調べていましたが、その時参考にした記事ではローカルDBとしてBoltを使っていました。

Bolt

GitHub - boltdb/bolt: An embedded key/value database for Go.

Boltは上記になります。READMEが結構充実しているので、使うときには一読したようが良さそうです。
Bolt自体がGolangで実装されているため、Golangで作るアプリの組み込みDBとして使うことができます。
データはバイナリとは別のファイルで独立して保管されます。

BoltはKeyValue形式のデータベースで、シンプルな作りを目指しているようです。
データの集合を表す「Bucket」の中に、一つのKey・一つのValueをbyte配列で格納します。
またこの「Bucket」をネストさせることもできるようです。

使ってみて

今回はBoltを使い、Todoを登録・参照・削除するコンソールアプリを作ってみました。
作ったものは以下になります。

GitHub - SrcHndWng/go-learning-boltdb-todo

先にも書いたように「Bucket」の中にデータを格納する方式です。
なので登録・参照・削除とも「Bucket」に対して操作を行うことになります。

ソースで表すと以下のようになります。

登録・更新

return db.Update(func(tx *bolt.Tx) error {
    b, err := tx.CreateBucketIfNotExists([]byte(bucket))
    (中略)
    err = b.Put(keybytes(id), []byte(data))

参照

db.View(func(tx *bolt.Tx) error {
    b := tx.Bucket([]byte(bucket))
    c := b.Cursor()
    for k, v := c.First(); k != nil; k, v = c.Next() {
        (何らかの処理)
    }

削除

db.Update(func(tx *bolt.Tx) error {
  b := tx.Bucket([]byte(bucket))
  err := b.Delete(key)
  return err
})