【Golang】Base62 に標準ライブラリで変換する【Base16 → Base62 のハッシュ値取得】

> Go 言語（以下 Golang）で、**数値を Base62 でエンコードしたい**。例えばハッシュ値や、バイナリ（`[]byte`）データなど。

そうです。Base64 ではなく、Base<ruby>62<rt>・・</rt></ruby> です。

しかし、[「"golang" base62 変換」でググって](https://www.google.com/search?q=%22golang%22+base62+%E5%A4%89%E6%8F%9B)も、<font color=darkorange>外部ライブラリを使う方法ばかりだった</font>ので自分のググラビリティとして。

- [Base62](https://en.wikipedia.org/wiki/Base62) @ Wikipedia
  - エンコードに使われる文字: `[0-9A-Za-z]`（`0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz` の 62 文字）

## TL; DR <sub><sup><sub><sup>（今北産業）</sup></sub></sup></sub>

1. Go 標準モジュールの [<code>math/big</code> パッケージ](https://pkg.go.dev/math/big)を使う。
1. `big.Int` 型には [`Text(n)` 関数](https://pkg.go.dev/math/big#Float.Text)があり `n` 進数指定できる。<br><sub><sup>62 進数の場合は、10 進数の `0〜9` --> `0-9`、`10〜35` --> `a-z`、`36〜61` --> `A-Z` を使い、合計 62 文字で `Int` を表現する。</sup></sub><br><sub><sup><strong><font color="darkorange">バイト・スライス（`[]byte`）を `n` 進数に変換したい場合</font></strong>は TS; DR 参照。</sup></sub>
1. マスター、<ruby>効くやつ<rt>動くもの</rt></ruby>をくれ。

	```go:10進数の61を62進数に変換、またはその逆に変換する例
	package main

	import (
		"fmt"
		"math/big"
	)

	func main() {
		// エンコード
		{
			i := new(big.Int)

			input := 61 // 61 = 62 進数 - 1 = 1 桁の最大値

			i.SetInt64(int64(input))
			fmt.Println("Base62 encoded:", i.Text(62))
		}

		// デコード
		{
			i := new(big.Int)

			input := "Z"

			i.SetString(input, 62)
			fmt.Println("Base62 decoded:", i)
		}

		// Output:
		// Base62 encoded: Z
		// Base62 decoded: 61
	}
	```

    - [オンラインで動作をみる](https://go.dev/play/p/nZRbFz736JD) @ Go Playground

    ```go:文字列をBase62にエンコード&デコード
    package main
    
    import (
    	"fmt"
    	"math/big"
    )
    
    func main() {
    	// エンコード
    	{
    		input := "string data to be encoded"
    		fmt.Println("Original input:", input)
    
    		bigInt := new(big.Int)
    
    		inputBytes := []byte(input) // 文字列をバイト変換しておく
    
    		bigInt.SetBytes(inputBytes)
    
    		fmt.Println("Base62 encoded:", bigInt.Text(62))
    	}
    
    	// デコード
    	{
    		bigInt := new(big.Int)
    
    		input := "58T1aST0vUU7M3Sc09PDm2WoJRLf8HPS96"
    
    		bigInt.SetString(input, 62)
    
    		fmt.Printf("Base62 decoded bytes: %v\n", bigInt.Bytes())
    		fmt.Println("Decoded input as str:", string(bigInt.Bytes()))
    	}
    
    	// Output:
    	// Original input: string data to be encoded
    	// Base62 encoded: 58T1aST0vUU7M3Sc09PDm2WoJRLf8HPS96
    	// Base62 decoded bytes: [115 116 114 105 110 103 32 100 97 116 97 32 116 111 32 98 101 32 101 110 99 111 100 101 100]
    	// Decoded input as str: string data to be encoded
    }

    ```

    - [オンラインで動作をみる](https://go.dev/play/p/B8DRuyP9Ljx) @ Go Playground

## TS; DR <sub><sup>マスター、もっと強いのくれ</sup></sub>

ハッシュ値の頭 n 桁を文字列で DB のキーとして扱いたいのですが、HEX でなく Base62 で欲しかったのです。

つまり、**ハッシュ値を Base16（16進数）でなく Base62（62進数）の文字列にして圧縮させる**ことで、短くさせつつも<ruby>コリジョン<rt>ハッシュ値の衝突</rt></ruby>の可能性をなるべく和らげたいのです。

**数値的にはエンコードを変えたところでコリジョン率は同じ**ですが、文字列でデータをやりとりする場合、同じ文字数なら情報の多い方がいいからです。

- ハッシュ？コリジョン？BLAKE3？ 👇
  - [最速のDBキー検索を求めて with SQLite3](https://qiita.com/KEINOS/items/c92268386d265042ea16#ts-dr-%E6%9C%80%E9%80%9F%E3%81%AEdb%E3%82%AD%E3%83%BC%E6%A4%9C%E7%B4%A2%E3%82%92%E6%B1%82%E3%82%81%E3%81%A6-with-sqlite3) | ハッシュ関数の基本と応用 @ Qiita

```go:xxHash3 アルゴリズムのハッシュ値を16進数＆62進数で表示する例
package main

import (
	"fmt"
	"math/big"
	"math/rand"

	"github.com/zeebo/xxh3"
)

func main() {
	// ハッシュ化したいデータ
	input := "Hello, 世界"

	// ソルト値
	salt := Salt()

	// ハッシュ化 （xxHash3)
	hashUint64 := xxh3.HashString(input + salt)
	fmt.Printf("Base16: %x\n", hashUint64) // 通常の16進数表現

	// big.Int型経由で任意進数に変換
	var i big.Int

	i.SetUint64(hashUint64)

	fmt.Println("Base16:", i.Text(16)) // 16進数表現
	fmt.Println("Base62:", i.Text(62)) // 62進数表現
}

// Salt はランダムな数値の文字列を返します。
func Salt() string {
	//nolint:gosec // not for cryptographical use
	return fmt.Sprintf("%d", rand.Int())
}

// 下記出力はランダムな salt 値により毎回変わります
// Output:
// Base16: 4b4e7561403341fc
// Base16: 4b4e7561403341fc
// Base62: 6sQZ9urnbbK
```

- [オンラインで動作をみる](https://go.dev/play/p/gcxrW1bvzKw) @ Go Playground
- [xxHash3](https://xxhash.com/) @ xxhash.com

:::note warn
<font color=red>【注意】</font>例えば、元データのハッシュ値が `0000004b56abd80ec...` のように偶然にも `0` で始まる場合、進数計算（変換）時にトリムされて `4b56abd80ec...` と短くなってしまいます。**ハッシュ値を固定長の文字列として Base62 で欲しい場合は注意**します。例えば、n 桁の固定長で欲しい場合は「同数の連続 `0` を頭に付け加えたのち、末尾 n 桁を利用する」などです。`fmt.Printf("%032x", myValue)` の利用を検討するといいでしょう。
:::

## 参考文献

- [hashアルゴリズムとハッシュ値の長さ一覧](https://qiita.com/KEINOS/items/c92268386d265042ea16) @ Qiita
- [Int.Text()](https://pkg.go.dev/math/big@go1.17.8#Int.Text) | Go v1.17.8 @ pkg.go.dev
- [Multibase format](https://github.com/multiformats/multibase) | multiformats @ GitHub
- [Multihash format](https://multiformats.io/multihash/) | Multiformats @ multiformats.io