ポインタの学び直し、参照とは違う

2時に寝て7時に起きた。深夜に葬送のフリーレン10巻を読んでからなんとなく眠れなくて夜更かししてた。木曜日は会議がなくて自分のために時間を使える。機能開発に集中して実装していた。

go の学び直し

Gopher塾 #4 - 私も解説できるポインタ - DAY1 に参加した。

今日のテーマはポインタ。tenntenn さんが話すのだから深い内部実装の話しなどもあるのかな?と期待していたけれど、これは基本的な go のポインタの扱いを学ぶ講義だった。私にとっては9割は知っていることだった。それでも1割は知らないことがあったので参加して勉強にはなった。この歳になると本やイベントから1-2割学べたら十分だと思う。go は内部的にすべて値渡しで何でもコピーするするといった振る舞いをする。ポインタを渡すと、ポインタの値であるアドレスをコピーすることでプログラムが動く。コピーなので大きな構造体をそのまま渡すとその分のメモリのオーバーヘッドがあって遅くなったりする。ポインタならアドレスだけのコピーで済む。

go には参照の概念はないという説明があって、ポインタと参照は別の概念なんだと今更ながらに気付いた。gpt-4 にポインタと参照の違いを尋ねたりしていた。参照は初期化後に変更できなかったり、null を参照できなかったり、ポインタ演算のようなことができなかったりすることで安全にプログラミングするための言語機能と言える。一般的には参照はポインタを使って実装される。ポインタの方が低レイヤで制約が少ないと言える。参照はポインタの一部の機能を安全にプログラマーに提供していると言える。

次に go のポインタの特徴をまとめる。

  • 型付け
    • ポインタは型付けされていて、特定の型の変数のアドレスだけがその型のポインタに割り当てられる
  • アドレス演算子とデリファレンス演算子
    • これらを単項演算子と呼ぶ
    • アドレス演算子 (&) で変数のアドレスを取得してポインタを作成する
    • デリファレンス演算子 (*) でポインタが指すアドレスに格納されている値にアクセスする
  • ポインタ演算制限
  • ポインタ演算は許可されていない、メモリアクセスの誤りやセキュリティ上の問題が軽減される
  • new と make 関数
    • new 関数を使うと指定された型の新しい変数を作成してそのアドレスを返す
    • make 関数は、スライス、マップ、チャネルなどの複合データ構造を作成および初期化して、それらへのポインタを返す
  • ポインタの nil 値
    • ポインタは、無効なアドレスを表す特別な値である nil をもてる
    • nil ポインタにアクセスしようとすると、実行時に panic が発生する
  • メソッドレシーバとしてのポインタ
    • メソッドレシーバとしてポインタを使うとメソッド内でレシーバオブジェクトの変更ができる
      • 値レシーバは意図せぬ不具合を招く可能性があるから基本的にはポインタレシーバを使う方がよいのではないかといった話しもあった

キャッシュ機構の導入

1時に寝て7時に起きた。リリース延期を決めたので3月末へのストレスやプレッシャーからは解放されていつもよりよく眠れた気がした。あくまで気がしただけ。

キャッシュライブラリの選定

ある機能を作るための準備としてキャッシュ機構を作ることにした。avelino/awesome-go を眺めて適当なキャッシュライブラリを選定する。シンプルな用途向け、オンメモリで goroutine-safe で保守されていて実績があるものでのバランスを取るときむらさんのキャッシュライブラリになった。

ちょうどいまのお仕事を始める前に スカウトをもらった会社 のテックリードがきむらさんだったので、いまはその会社にいるんだと思い出したのが半年前。きむらさんとはリアルで何年も会っていないし、仲がよいわけでもないけれど、なぜか facebook でも繋がっているので存在を忘れるということもない。

閑話休題。ある特定の mongodb コレクションのデータをキャッシュしたい。

キャッシュの生成。

cache := gcache.New(128).Build()

特定の用途で一部の処理だけ使いたいのでこんな感じでキャッシュの処理を実装した。

value, err := cache.Get(id)
if err == nil {
    return value.(*myData), nil
} else if err != gcache.KeyNotFoundError {
    log.Error("failed to get value from cache", map[string]any{
        "key": id,
        "err": err,
    })
}

// 通常処理

cache.Set(id, setting)

念のため、キャッシュをすべてクリアするときは次を呼ぶ。これをキャッシュ制御 api から呼び出せるようにしておく。何らかの理由やバグなどでキャッシュをクリアする必要もあるだろう。

cache.Purge()

データを削除したときは id 指定でキャッシュを削除する。

cache.Remove(id)

キャッシュが使われているかどうかは内部的に統計を取っているのでそれをキャッシュ制御 api から取得することでわかる。 キャッシュが使われていれば HitCount が増え、mongodb にアクセスしていれば MissCount が増える。単体レベルでプログラムのデバッグにはなる。

return CacheStat{
    HitCount:    cache.HitCount(),
    MissCount:   cache.MissCount(),
    LookupCount: cache.LookupCount(),
    HitRate:     cache.HitRate(),
}

リリース延期

3時に寝て7時に起きた。昨日は遅くまで確定申告の準備をしてたから寝坊した。その分8時半に申告会場に立ち寄ってからオフィスに出社したので普段よりも朝はゆっくりできた。

windows インストーラー開発

windows のインストーラーを作る方法は2-3種類ほどあるそうだけど、もっとも標準的な visual studio に付属しているツールでインストーラーを作っている。特性の違う2つのアプリケーションを1つのインストーラーで扱っていると、アップデートやアンインストールでいろいろ不都合が生じるように思えたのでインストーラーを2つに分割したらよいのではないか?と先週末に助言をしていた。すると、1つのアプリケーションを2つのインストーラーを使ってセットアップするようなものを作ってしまって、複雑さはなにも解消していなくて、全然意図したものではないと訂正して作り直してもらうことになった。

私の指示が2つに分割すればよいという曖昧な助言だったのもよくないけれど、1つのアプリケーションを2つのインストーラーで操作するという発想が私の中になくて勘違いする余地があったんやと失敗体験となった。

一方でメンバーが「最初からそう言ってくれればよかったのに、、、」とか言い始めて、この姿勢はよくないなとも感じた。この問題の責任は指示が曖昧だった私にあるのは間違いないが、1つのアプリケーションを2つのインストーラーでインストールするという考え方に疑問を抱かないのは怠慢だし、実際にそのやり方で問題は解決していなかった。なんのために設計するかを当事者意識をもって開発していたら、言ってくれなかったから分からなかったという考え方にはならない。勘違いしていたとか、気付きが足りなかったとか、そういう言葉がほしかった。

定例会議で、意図しているのは、特性の違う2つのアプリケーションをそれぞれ独立させて制御すればシンプルになるよと説明して、もう一度作り直してもらって、結果として意図した通りシンプルにインストールを扱えるようになった。

リリース延期

昨日書いた通り、基準となる issue をすべて fix できないことが確認できたのでそれを説明して4月末まで延期とした。これにより、遅れていた機能開発には十分な期間を取れるし、QAテストのための準備にも余裕をもてる。インフラやドキュメントを整備する時間もあるだろう。

今月の私の稼働時間は (契約は160時間だけど) おそらく200時間を余裕で越えるだろうし、記帳していない稼働時間も含めれば230時間ほどはいくのではないかと思う。私の中では納期に間に合わせるためにがんばる意思はあったんだけど、クリティカルパスはすべて私が担当するといった調整をできなかったので仕方ないなといった落とし所で来月への延期を正当化した。

期日前の敗北

0時に寝て6時半に起きた。日曜日に1ヶ月振りのお休みをとったので体力はぐっと回復した。

リリース判定前

明日をリリース延期を判断する期日としている。明日でリリースまでに対応しないといけない enhance ラベルが付いたタスクを fix できなかったら延期を決定すると先週の定例で基準を決めた。私がやろうと思っていたタスクは今日すべて fix したけれど、全体として fix できそうにないタスクが2つ残っている。さらに今日、検証していてリリースまでにやった方がよい enhance を2つみつけたので1ヶ月延期した上で、この2つも追加でやってしまおうかと考えている。事実上、今日の進捗をみた時点で明日の定例会議では延期の決断をする。がんばったけど、ダメなもんはダメなので潔く敗北を認めて残っている開発課題を今月末までに fix するようにスケジュールを調整していく。その分 QA テストのための工数を多く取れるのでテストツールを作ったり、結合テストを追加したりなど、余裕があれば品質を上げるためにできることも増える。1ヶ月前倒しにしていたから元の計画に戻るだけだし、私が管理対象にしていないモジュール群の開発遅れ (開発は不要と当初言われてた) なのでそもそも計画になかった。計画になかったとはいえ、その対応への気付きが遅れて、担当変更が遅れて、最終的にリリースが遅れたことはマネージャーとしての私の責任なのでシンプルに悔しい。もっとうまいやり方はあったはず。

2022年度の個人の確定申告

昨年の確定申告はこちら

一旦20時頃に家に帰って晩ご飯を食べてから21時にオフィスに戻ってきて確定申告の作業を始めた。15日(水)までなのでわりとぎりぎり。本当は日曜日にやろうと思っていたのにお休みしてしまったのでこんな時間から始めることになった。確定申告も例年と同じになっていて、且つ、いまは副業を受けていないので特別な売上や経費の登録をすることもない。取引一覧でやることは次の2つ。

  • 源泉徴収済みの印税収入を、売上と源泉徴収税の2つの明細として登録し直す
  • 寄付金の明細登録

これは15分ほどで完了してそれから確定申告書を作る。freee 確定申告 を5年以上使っている。その後の手続きも決まっていてワークフローに従ってこれらの入力を行う。

  • 源泉徴収票からの転機
  • 印税売上を源泉徴収済み雑収入として登録
  • ふるさと納税と npo 向けの寄付金の登録
  • 株式の取引報告書の登録 (損益通算)
  • 小規模企業共済の所得控除の登録

印税売上は源泉徴収済みなので無視してもよいが、寄付金や共済の所得控除があるので毎年行うことで節税になる。書類はすべて揃っていたので証明書を確認しながらワークフローを進めて3時間もあれば確定申告書を完成した。プリンタで紙に印刷して2箇所にマイナンバーを記載して、台紙に寄付金と共済の証明書を添付する。この証明書の添付をどうやって電子化して申告したらよいかわからないので毎年紙で提出している。

春休み

目次

朝起きたんだけど、なんかしんどくてそのまま寝てだらだらしてた。今日は久しぶりにお休みしてた。

夕方からお休み

2時半に寝て8時に起きた。お昼から少し作業して夕方にお蕎麦食べに出掛けてそれから家に戻って休んでたらそのまま寝てた。

ストレッチ

今日の開脚幅は開始前157cmで、ストレッチ後159cmだった。ここ1ヶ月ほど患っていたすねの外側の筋の張りはまだ残っているものの先週より少しよくなった。復調の兆しがみえてきてよかった。出張戻りはいつも体調が悪めなものの、悪いだろうと予期していたほどは悪くなく、それまでの大量がよくなかった分だけ復調しているようにも感じ取れた。いまの状態は複雑。いつも担当してもらっているトレーナーさんが会社の技能試験のようなイベントの全国大会に出るという。全国で予選を勝ち抜いた人だけが参加できるそうで、200店舗以上ある中での予選を勝ち抜いた数人 (十数人?) が選抜されて全国大会に出れるという。それなりの倍率の予選を勝ち抜いたということなので改めてこのトレーナーさんはすごかったんやと見直した。全国大会もがんばってほしい。

昔の上長の背中を追う

2時に寝て7時半に起きた。昨日もWBCをみてから晩ご飯を食べて軽く作業してそのまま寝た。

伴奏

ここ2-3日若いメンバーの開発をサポートしている。

「◯◯ができません」

私が5分ほどでググってできそうなドキュメントや so をみつけてリンクを貼る。

「できました。」

みたいなやり取りを何度かした。大して難しくない処理を実装できないのは公式ドキュメントをちゃんと読んでいないのと、インターネットの検索方法を習得していないように私からはみえる。一度、定例会議のときに google の言語設定を英語にした方がよい。日本のキュレーションサイトの記事の品質は低いからと伝えたが、まだそれを実践しているようにはみえない。いまや英語は deepl で翻訳すれば大半を斜め読みできる。私も deepl で読んでいると教えたりしているのだけど、日本語の記事しか検索していないから未知のことをできないとなってしまう。

チャットで困っていることや問題点を整理したり、どういう視点で調べていくかをやり取りしながら本人が理解して作業できるようにサポートしている。私がやれば10分で終わるようなことを2時間ぐらいかけてチャットしている。それで昼間は自分のお仕事をやらずチャットの話し相手を務めている。誰もが最初は初心者なのでそういう時期はある。以前は質問すらできていなかったところを、あれができないとか、これがわからないとか質問できるようになったというのは成長したと受け取れる。わからないことを説明してもらうことで、私も相手のことを理解できて適切な指示や指導ができる。その過程でプログラミングの理解度も測れるので issue をアサインするときの参考にもなる。

曖昧なことをチャットで聞くのは効率が悪いから、口頭であれこれ質問してくれるようになるのがこの次のステップかな。以前と比べて質問してくれるようになったので信頼関係は少しずつ構築できてきつつあるのかもしれない。

昔の自分といまの自分

既存のある java のコードを読んでいて、私の中ではワーストから数えた方が早いほどのひどいコードをみている。java の言語仕様もプログラミングもどちらもよくわかっていない人がキュレーションサイトにあるような記事を読んで動くように作ったようなツールにみえる。アリエルを辞めてからいくつかの会社で働いてきて java のコードも読み書きしてきた。これまでの経験からその職場での java のコードは品質が高かったし、私もその影響で java の設計やアーキテクチャにも関心をもつようになった。私は未熟なので人並み程度のプログラミングしかできないが、そのスキルを底上げしてもらったのはその職場での3年間の java 開発といえる。私にとっての普通が当時の開発体験やチームの同僚になったことでそれ以降に出会った開発者の大半はスキルが低いようにみえてしまう。そして、その後に私がどんなプロダクトを開発しても決して当時の先輩方に敵うことはないと慢心することもない。だからプロダクトの開発を終えて、組織の方向性にあわなければすぐに辞めることもできた。

いま私がメンバーに教えていることも、メンバーからみたら少し厳しくみえるかもしれない。私にとって先人のような偉大な開発者に自分もなれるんやろか?とか思いながらマネジメントをしていたりする。今週はとくに18時にホテル戻って2-3時間寝て22時頃から2-3時間コードを書いたりしていた。当時の上長もよくそうやって開発していた。当時の私はよく働いたが、そんな私からみてもその上長もよく働いていた。そして上長の生産性は私よりも数倍高かった。お互いに課題管理システム上にいることはわかっていたし、夜中の1時頃にチケット上でやり取りすることもあった。私もいま当時の上長と同じようなことをやっているなと感慨に浸りながら夜中にコードを書いていた。うちのチームのメンバーは誰も夜中に開発していないことだけが当時とは違うことにも気付いた。

リリース延期の危機

1時に寝て7時に起きた。いつも通り夕方にホテルに戻って2時間ほど寝て起きたらテレビで WBS をやっていてそのままみてた。そしたら晩ご飯食べるタイミングも作業するタイミングも逃して日記を書いたり雑多なことをしていた。

プロジェクトの進捗報告

出張したときの月例報告の4回目。前回の進捗報告はこちら 。1月にリリース前倒しを提案して颯爽と1ヶ月前倒しをしたのにその翌月に現時点ではまだリリース可能かどうかを判断できないといったことを報告した。まったく情けない。サーバーサイドとフロントエンドの開発はすでに完了しているのに。しかし、もともとこのプロジェクトの開発対象に入っていなかったもので、ほぼ完成していると聞いていたモジュール群の半分が機能不足や低品質で作り直すことになった。残りの半分もそのままでは動かない。

経験の浅いメンバーに1ヶ月以上の時間を与えて作り直してもらうようにお願いしていたが、うまく進捗せず時間だけが過ぎていって、結果的に2月の半ばから私が引き取って大半の機能を開発している。結果的にそのメンバーにお願いしていた開発の8割を私が2週間でほとんど実装した。2月の中旬から私がずっと休祝日に開発のお仕事をしていたのはこの開発遅れを補填するためだった。チームが fix した2月の issue 数が57でそのうちの30を、enhance ラベルが付いたものは28でそのうちの13を私が担当した。今月の半分の開発を私が代替わりして帳尻を無理やりあわせた。もはや遊撃のレベルではなく、私が本気出して全部作っておきましたみたいなことをした。

本当はメンバーに開発経験をつけてもらうために私がいるので私が主担当で開発するのはよくない。とはいえ、このままいくと2ヶ月ほど開発遅延する、しかもこのプロジェクトの中核でもない機能のために、それも悔しいし、うちの会社の信頼にも関わるのでズルしてしまいましたと経営陣へ正直に報告した。自分がやるよりも他人に教える方がずっと難しい。先方からは咎めるものではなかったし、私が開発して帳尻をあわせるのを止めるものでもないという承認は得た。

難しい開発課題を経験の浅いメンバーに担当させてしまった私のマネジメントの誤りであることは、チームのふりかえりでも、経営者への報告でも伝えている。なにが起ころうとプロジェクトの責任はマネージャーの私にあることは理解している。その遅れはマネージャーが責任をもって対応するのだとメンバーが学ぶ機会にもなったんじゃないかという意見も出た。私も過去にそういう上長をみて思うところはあったのでそれはそうかもしれない。なぜ1ヶ月以上も時間を与えているのに芋づる式にスケジュールが遅延するのか。その要因もメンバーの行動や進捗をみていて理解できた。第一に経験が浅いために開発の見通しや見積もりを立てられない。例えば課題が3つあるとして、1つしかみえていないから「できそうです」と言っていても、1つ終えた後にまた1つありましたと報告があり、その1つを終えてもまだもう1つありましたと報告が来る。一定の経験があれば作業を開始する前に3つあることを整理して、その上で納期にあわせて3つを対処する。納期いっぱい使って1つだけやろうとするところの意識の差は大きい。第二に期日までに実装できる一定のスキルをもっていないとコードレビューが1週間とか続いてしまう。そういった開発者にクリティカルパスとなる issue を担当させてはいけないように思えた。

ある issue がクリティカルパスになってしまった時点で、私かスキルのあるメンバーのどちらかへ引き継ぐように2月中旬に調整していればいまの状況は変わったのではないだろうか。その判断が2週間遅れたことに今回は気付けた。結果論ではあるが、厳しい判断をもう少し早めに下さないといけなかった。

windows のサービス起動を go から制御する

2時に寝て3回ほど起きて7時に起きた。淡々と自分の機能開発をしていた。ある処理が失敗したときにローカルのファイルシステムに暗号化して書き込み、定期的にそのディレクトリを監視してファイルがあれば読み込んで復号化してリトライをするといった仕組みを実装した。とくに難しくなくすぐにできた。

晩ご飯に 大衆酒場 PING (ピン) というお店に行ってみた。1人でも入りやすく値段も安く食べ応えもあっても私のイメージする居酒屋さんの印象にぴったりでよかった。また出張したら行こうと思う。そろそろ出張でバテてきたので今日は夜にお仕事せずに晩ご飯食べてからもテレビをみながらだらだらしてた。

windows のサービス起動

準標準パッケージである golang.org/x/sys パッケージを使ってアプリケーションの起動と停止をサービス管理画面から操作できるようにする。サンプルコードは次の場所にある。

サービスから呼ばれたかどうかを判定をすることでエントリーポイントを切り替えられる。

inService, err := svc.IsWindowsService()
if inService && err == nil {
    if err := runService(serviceName, false); err != nil {
        log.Error("failed to run service", map[string]any{
            "err": err,
        })
        return
    }
}

そして、次のような Execute() メソッドをもつ windows サービスから呼ばれる構造体を定義して、そのメソッド内でサービス管理画面からのステータス変更に対応する状態遷移のコードを実装すればよい。ここではサービス開始してから stop/shutddown で停止するぐらいしか必要ないのでシンプルに実装した。一時停止や再開も必要ならもう少し複雑なコードになる。

type myService struct{}

func (m *myService) Execute(
	args []string, reqCh <-chan svc.ChangeRequest, statusCh chan<- svc.Status,
) (ssec bool, errno uint32) {
	ctx, cancel := context.WithCancel(context.Background())
	ch := startMyService(ctx, getConfig())
	statusCh <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop | svc.AcceptShutdown}
	stop := false
	for {
		select {
		case c := <-reqCh:
			switch c.Cmd {
			case svc.Stop, svc.Shutdown:
				stop = true
				cancel()
				break
			}
		}
		if stop {
			break
		}
	}
	statusCh <- svc.Status{State: svc.StopPending}
	<-ch // wait until MyService would be completed
	return
}

暗号化と復号化を実装してみる

不定期に寝て7時半に起きた。起きてからシャワー浴びたらしゃんとした。昨日と同様に昼間働いて、ホテルに戻ってきて2-3時間寝てから夜に2-3時間かけて次のコードを書いた。

go での暗号化/復号化

ある機密情報を扱うので暗号化して普通には「みえない」ようにしたい。まったくわからないのでググったら次の記事がみつかった。

まずはこの記事にあるサンプルコードを動かしてみながら内容を理解する。暗号技術そのものは理解できないが、コードの振る舞いそのものは動かしながら理解できる。その後、いろいろ調べているとこのサンプルコードの大半は crypto/cipher パッケージにあるサンプルコードと同じであることに気づく。標準ライブラリのドキュメントでは iv (initialization vector) を暗号化対象の文字列の一部を切り取って生成している。iv は一意である必要はあるが、セキュアでなくてもよいとある。よくあるやり方とあるのでサンプルコードをみながら同じように実装した。

// The IV needs to be unique, but not secure. Therefore it's common to
// include it at the beginning of the ciphertext.

最終的な暗号化と復号化のコードは次になった。標準ライブラリにあるサンプルコードと基本的には同じ。

func Encrypt(secret string, plainText []byte) ([]byte, error) {
	block, err := aes.NewCipher([]byte(secret))
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}
	cipherText := make([]byte, aes.BlockSize+len(plainText))
	iv := cipherText[:aes.BlockSize]
	if _, err := io.ReadFull(rand.Reader, iv); err != nil {
		return nil, fmt.Errorf("failed to read for iv: %w", err)
	}
	stream := cipher.NewCFBEncrypter(block, iv)
	stream.XORKeyStream(cipherText[aes.BlockSize:], plainText)

	buf := make([]byte, base64.StdEncoding.EncodedLen(len(cipherText)))
	base64.StdEncoding.Encode(buf, cipherText)
	return buf, nil
}
func Decrypt(secret string, text []byte) ([]byte, error) {
	block, err := aes.NewCipher([]byte(secret))
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}

	dbuf := make([]byte, len(text))
	n, err := base64.StdEncoding.Decode(dbuf, text)
	if err != nil {
		return nil, fmt.Errorf("failed to create chiper: %w", err)
	}
	cipherText := dbuf[:n]

	if len(cipherText) < aes.BlockSize {
		return nil, fmt.Errorf("ciphertext is too short: %d", len(cipherText))
	}
	iv := cipherText[:aes.BlockSize]
	cipherText = cipherText[aes.BlockSize:]
	stream := cipher.NewCFBDecrypter(block, iv)
	// XORKeyStream can work in-place if the two arguments are the same.
	stream.XORKeyStream(cipherText, cipherText)
	return cipherText, nil
}

いろいろな変換

前日は0時ぐらいまでオフィスでお仕事していて、家に帰ってお風呂入って荷造りして、そのまま寝ないで5時過ぎに出かけて新神戸駅から6時10分発の始発に乗ってから2時間ほど寝た。昼間働いて18時にホテルに戻ってまた2-3時間ほど寝て晩ご飯食べてから2時間ほどお仕事していた。寝付けなくて3時ぐらいに起きて吐いた。吐いたら気分よくなってその後はよく眠れた。

windows の内部の文字の扱い

前日の日曜日に実装したツールをメンバーに windows server検証してもらう。ある変換処理で windows の内部では文字を utf16le で扱っていて、文字コード変換の処理が漏れていることに気付いた。go の準標準ライブラリを使って、次のようなコードで utf16le から utf8 への変換ができる。

import "golang.org/x/text/encoding/unicode"

decoded, err := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder().Bytes(b)

LDAP の DN の仕様

LDAP の distinguished name (DN) を調べた。例えば、windows で dn を powershell の Get-ADUser を使って取得すると次のような値を取得できる。

dn="CN=Test,CN=Users,DC=example,DC=com"

CNDC は大文字/小文字どちらでもよいが、それが違うと dn の値として別の値になってしまう。できれば小文字に統一したい。その変換処理を実装しようと思って仕様を調べていたら RFC-4514 Lightweight Directory Access Protocol (LDAP): String Representation of Distinguished Names で定義されていることがわかった。ここで定義されている特殊な文字、例えばカンマや等号などはエスケープして値としても使えるように書いてある。キーの値を変換するにはこれらの仕様を理解してパースして変換しないといけない。わりと大変なことを理解して go-ldap のライブラリのコードを読んでいたら正に ParseDN() がそんな実装になっていた。

func ParseDN(str string) (*DN, error)

このツールを使って dn インスタンスにパースして文字列表現を取得すると自動的に正規化されて小文字に変換してくれる。

dn, err := ldap.ParseDN(s)
if err != nil {
    return fmt.Errorf("failed to parse dn: %w", err)
}
dn.String()