Nix Flakes による Go + Protocol Buffers 開発環境の紹介

# Nix Flakes による Go + Protocol Buffers 開発環境の紹介

## 概要

Nix パッケージマネージャの機能の一つである **Nix Flakes** を使って， Go 言語における Protocol Buffers を扱える開発環境を構築したときのメモです．理解した内容の整理も兼ねて公開します．

Nix に関連する日本語の情報はとても少ないため，布教の目的で Nix 自身についての簡単な紹介もします．  
実用的な使用例をみてもらうことで，興味を持っていただける人がいたら嬉しいです．  
それに加えて Protocol Buffers についての紹介を行い (#技術的背景)，本題に入ります．

※この記事の内容は筆者の理解で書かれています．  
自分自身使いながら覚えていっているので，もし間違っている点などあればご指摘いただけると嬉しいです．

## 技術的背景

Nix / NixOS / Nix Flakes, Protocol Buffers についての紹介を行います．  
Nix 関連については過去にも似たような [紹介記事](https://zenn.dev/kino_ma/articles/f2a4df384b91a4) を公開しているので，興味があればそちらもご参照ください．  

本記事では軽い紹介にとどめ[^introduction]，詳しい使い方やインストール方法については引用先の公式ページ等をご覧ください [^installation]．

[^introduction]: Nix の布教のためにまずはいろんな人に興味を持っていただきたく思っているためです
[^installation]: 気が向けば，いずれ別で記事にします

### Nix

Nix とは，ざっくり言うと「色々なことを宣言的に管理して便利にしよう！」という思想のもと開発されているソフトウェア群です．

一般に「Nix」と呼ばれるものには大きく三種類あります [^nix]．

[^nix]: 公式の説明文を読むと， *Nix* という言葉が指すものは厳密には Nix パッケージマネージャのようです．が，初めて触れる時には混乱し得ると思うので，それぞれの説明をしてみます

- [Nix 言語](https://nixos.org/guides/nix-language.html)
    - 純粋関数型言語である
    - 分類としては YAML, JSON に近い
- [Nix パッケージマネージャ](https://nixos.org/manual/nix/stable/introduction.html)
    - NPM における `package.json` のようなものを Nix 言語で宣言的に記述するパッケージマネージャである
        - macOS の brew や Debian 系 OS の apt のような存在
    - ビルドの再現性が高いこと， Docker のように環境の隔離を行えること (nix-shell) などが特徴
- [NixOS](https://nixos.org/)
    - Linux ディストリビューションである
    - Nix 言語で多様なシステム設定を宣言的に行うことができる
    - Nix パッケージマネージャが標準で付属している

この三つをごちゃ混ぜに「Nix」と呼ぶことが多いと思います．  
そもそも，役割としてこの三つが強く結びついているので，セットとして意図的に「Nix」と単に呼ぶ場合もあります．

### Nix Flakes

https://nixos.wiki/wiki/Flakes

先述した Nix パッケージマネージャの一つの機能です．  
パッケージを，より再現性と発見可能性の高い (discoverable) 形 = *Flake* で定義し，取り扱うことができます．  
その再現性は，Git のコミットハッシュやソースコードの SHA256 ハッシュを結びつける形で依存関係を整理することなどで実現されています．

Nix Flakes は，例えば以下のような機能を持ちます [^flakes]．

[^flakes]: https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html

- パッケージのビルド方法を定義し，出力を得る (`nix build`)
- `docker run ... bash` のように，隔離された環境の開発用シェルを使うことができる (`nix develop`)
- 他人の作った Flake の出力コマンド等をインストールする (`nix profile install`)
- NixOS のシステム環境設定を Flake として記述する

この特性から，開発環境として Docker の代わりに使ったり， CI/CD 用途やサーバ用途にも適しています．

自分もまだ全容を掴んでいるわけではありませんが，今のところとても便利です．  
最近のプロジェクトでは，実験的に Docker の代わりとして使うようにしています．

#### Nix Flakes の使用例

使用例は本記事の本題として紹介する他，筆者が過去に公開した [Nix Flakes の紹介記事](https://zenn.dev/kino_ma/articles/f2a4df384b91a4#nix-flakes-%E3%82%92%E4%BD%BF%E3%81%84%E5%A7%8B%E3%82%81%E3%82%8B) にも例があります．

### Protocol Buffers

https://developers.google.com/protocol-buffers

Protobuf とも呼ばれます．

Google が作った (?)，言語・プラットフォーム非依存な，データ構造をシリアライズするための仕組みです．  
やることは XML や JSON に似ていますが，それらと比べて smaller, faster, and simpler とのことです．  
定義されたデータ構造は，通信プロトコルやデータ保存に活用することができます．

Protobuf はデータ構造の定義をサポートしますが，これに遠隔手続き呼び出し (**Remote Procedure Call**) の定義を加えたものが gRPC のようです．

Protobuf を使ってデータの構造を定義すると，各プログラミング言語のライブラリを使って簡単にシリアライズ・デシリアライズすることができます．
Protobuf によるデータ構造の定義は， `.proto` ファイルに記述されます．  
`protoc` コマンドがこの `.proto` ファイルを読み出し，定義されたデータを各言語で読み書きするためのコードを自動生成してくれます．

#### `.proto` ファイルの例

[公式の example](https://github.com/protocolbuffers/protobuf/blob/37a4656ad4193c2e34d75c14a8ffc3a66c420fd1/examples/addressbook.proto) にあるものの抜粋です．

```addressbook.proto
syntax = "proto3";
package tutorial;

import "google/protobuf/timestamp.proto";

message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
```

この protobuf 定義ファイルでは， `Person` というデータの構造 (`message`) を定義しています．  
冒頭のオプション関係を除けば，全ての内容が `Person` に関連する構造を定義しているものです．

プログラミング言語っぽい見た目をしているため，眺めればなんとなく意味をつかめると思います．  
ただし， `= 1` などの部分はいわゆる代入ではありません．  
ある `message` に存在する各フィールドには，ユニークな識別子 *tag* がついている必要があり， `=` 記号でこの tag を結びつけているのです．  
シリアライズなどの際にこの tag を使用するようで，番号の重複や使い回しが許されないなど，制約を持ちます．

## Nix Flakes による Go + Protocol Buffers 開発環境

本題に入っていきます．  
Nix Flakes を使った Go + Protocol Buffers 開発環境の作り方です．

といっても， Nix Flakes が有効な環境であれば，準備は以下の `flake.nix` を作るだけです．

```flake.nix
{
  description = "";

  inputs = {
    nixpkgs = { url = "github:NixOS/nixpkgs/nixpkgs-unstable"; };
    flake-utils = { url = "github:numtide/flake-utils"; };
  };

  outputs = { self, nixpkgs, flake-utils }: 
    flake-utils.lib.eachDefaultSystem (system:
      let
        inherit (nixpkgs.lib) optional;
        pkgs = import nixpkgs { inherit system; };
      in
      {
        # Go コンパイラ， Go 向け Protobuf コンパイラが有効な開発用シェル
        devShell = pkgs.mkShell {
          buildInputs = [
            pkgs.go
            pkgs.protobuf
            pkgs.protoc-gen-go
          ];
        };
      });
}
```

このような `flake.nix` がある状態で `nix develop` とすれば， `go`, `protoc` コマンドが有効な開発用シェルに入ることができます．

開発環境の作り方としては以上ですが，ここからは実際に開発を行う際の手順をざっくり説明していきます．

### 開発の際の手順

実際に開発を行う際の手順をざっくり説明していきます．

なお，貼ってあるサンプルコードは GitHub に公開しています．

https://github.com/kino-ma/nix-flakes-go-protobuf-sample


#### 1. `flake.nix` を作る

上にある `flake.nix` を，対象 Go プロジェクトのディレクトリに貼り付けます．

もし既に `flake.nix` が存在する場合は，うまいことマージします．  
大事なのは `devShell = ...` の部分です．

#### 2. Protobuf ファイルを書く

扱いたいデータの定義を含んだ `.proto` ファイルを作成します．

例は技術的背景の章でも出しましたが，よりシンプルな例なら以下のようなものです．  

```sample.proto
syntax = "proto3";
package sample;
option go_package = "./main";

message Test {
    string some_text = 1;
}
```

`option go_package` には，生成される Go コードがどのパッケージに属するかを書きます．  
今回は `./main` としています [^pkg]．

[^pkg]: Go のモジュール管理を全く理解していないため，正しいやり方は別にある可能性が高いです

#### 3. 開発用シェルに入り，自動生成の Go コードを得る

Go コードの自動生成は， `protoc` コマンドを使って行います．

`protoc` などのコマンドを使うために， `flake.nix` で定義した開発用シェルに入ります．  

```sh
nix develop
```

この時，依存パッケージのダウンロードなどが入ります．

それが終わってシンプルな bash プロンプトが現れたら，以下のコマンドを打ちます．

```sh
bash-5.1$ protoc -I=. --go_out=paths=source_relative:. sample.proto
```

すると，以下のように自動生成された Go コードを含む `sample.pb.go` が生成されます．

```sample.pb.go
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// 	protoc-gen-go v1.28.1
// 	protoc        v3.21.8
// source: sample.proto

package main

import (
	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
	reflect "reflect"
	sync "sync"
)
// ...
```

ちなみに， `^D` で開発用シェルを抜けると， `protoc` コマンドも `go` コマンドも使えないことがわかります [^outside]．  
このように，そのプロジェクト専用の隔離環境を作るツールとして Flake は便利です．

[^outside]: ホスト環境にインストールしていなければ

```sh
bash-5.1$ ^D
$ go
zsh: command not found: go
$ protoc
zsh: command not found: protoc
$ which go
go not found
$ which protoc
protoc not found
```

#### 4. 構造体を Go プログラムで扱う

*3.* で自動生成した Go プログラムを読み込むことで，定義したデータ構造を扱えるようになっています．

適当に以下のような Go プログラムを実行してみます．  
同じパッケージである sample.pb.go から `Test` 構造体の定義を読み込み， protobuf の機能を使って読み書きするものです．

```main.go
package main

import (
	"fmt"

	"google.golang.org/protobuf/proto"
)

func main() {
	test1 := &Test{
		SomeText: "hello",
	}

	// []byte に変換
	out, _ := proto.Marshal(test1)

	test2 := &Test{}

	// []byte から再度 Test{} に変換
	_ = proto.Unmarshal(out, test2)

	fmt.Println(test2.SomeText)
}
```

```sh
bash-5.1$ go run .
hello
```

このように， `proto.Marshal` / `Unmarshal` を使って構造体を `[]byte` と相互変換できます．  
今回は `[]byte` との相互変換のみ行いましたが，このバイト列をファイルに書き込んだり，ネットワークを通じて送受信するなど，さまざまな使用方法が考えられます．

## まとめ

本記事では， Nix Flakes によって Go 言語 + Protocol Buffers 開発環境を構築する方法を紹介しました．  
また， Nix / Nix Flakes， Protocol Buffers についての簡単な紹介を行いました．

本記事の目的の一つは，Nix / Nix Flakes の実用的な使用例を紹介することで，興味を持ってくれる方を増やすことでした．  
一つ `flake.nix` ファイルを書くだけなので，とても簡単だと感じていただけたのではないかと思います．  
また， Protocol Buffers を初めて触る機会があったので，覚えたことの整理も兼ねていました．

Nix 関連の記事はまた書くつもりでいるので，よければそちらも読んでみていただけると嬉しいです．

以上です．読んでいただきありがとうございました．

## 参考文献

- https://developers.google.com/protocol-buffers/docs/gotutorial: Go + Protobuf のチュートリアル
- https://developers.google.com/protocol-buffers/docs/proto3: Protobuf 3 の言語ガイド
- https://developers.google.com/protocol-buffers/docs/reference/go-generated: 自動生成された Go コードについての説明
- https://pkg.go.dev/google.golang.org/protobuf/proto: proto ライブラリの GoDoc
- https://github.com/golang/protobuf/issues/872: 自動生成コードの出力先をカレントディレクトリにする方法