フューチャー技術ブログ

Go1.19で追加されたAppend系メソッド

はじめに

TIG/DXユニットの宮永です。

Go 1.19リリース連載の3本目です。本記事ではGo1.19のライブラリマイナーチェンジの中でも、Append系メソッドに注目して紹介します。

Go1.19でAppend系のメソッドが追加されたのは以下2つのライブラリです。
それぞれのProposalのリンクを添付します。

順に説明します。

encoding/binary

The new interface AppendByteOrder provides efficient methods for appending a uint16, uint32, or uint64 to a byte slice. BigEndian and LittleEndian now implement this interface.
Similarly, the new functions AppendUvarint and AppendVarint are efficient appending versions of PutUvarint and PutVarint.
Go 1.19 Release Notesより引用

encoding/binaryパッケージにはAppendByteOrder、AppendUvarint、 AppendVarint、が新たに追加されました。

(#50601)では、AppendByteOrderインターフェースの導入が提案され、BigEndian 型と LittleEndian 型に新たにAppendUint16、AppendUint32、AppendUint64が実装されています。
(#51644)では(#50601)にてAppendByteOrderを導入した関連でAppendUvarint、 AppendVarintを実装しています。この2つのメソッドは型への紐づけは行われておらずそれぞれuint64int64を引数に持ちます。

利用方法

byte型のスライスに整数型をさらに追加する場合、使用します。

Go1.18以前で上記を達成する場合、以下の様な実装をする必要がありました。

✅利用方法という観点では、今回導入されたメソッドはどれも同じ様な使われ方のため、AppendUint16に注目して解説します。


playground

go1.18以前
package main

import (
"encoding/binary"
"fmt"
)

func main() {
in := []uint16{102, 117, 116, 117, 114, 101, 32, 116, 101, 99, 104, 45, 98, 108, 111, 103}
out := make([]byte, 0, len(in))
for _, v := range in {
var arr [2]byte
binary.LittleEndian.PutUint16(arr[:], v)
out = append(out, arr[:]...)
}
fmt.Printf("raw: %v \n", out)
fmt.Printf("str: %s \n", out)
}

▼実行結果

raw: [102 0 117 0 116 0 117 0 114 0 101 0 32 0 116 0 101 0 99 0 104 0 45 0 98 0 108 0 111 0 103 0]
str: future tech-blog

今回導入されたAppendUintメソッドを使用すればより端的に実装することができます。

playground

go1.19以降
package main

import (
"encoding/binary"
"fmt"
)

func main() {
input := []uint16{102, 117, 116, 117, 114, 101, 32, 116, 101, 99, 104, 45, 98, 108, 111, 103}
out := make([]byte, 0, len(input))
for _, v := range input {
out = binary.LittleEndian.AppendUint16(out, v)
}
fmt.Printf("raw: %v \n", out)
fmt.Printf("str: %s \n", out)
}

▼実行結果

raw: [102 0 117 0 116 0 117 0 114 0 101 0 32 0 116 0 101 0 99 0 104 0 45 0 98 0 108 0 111 0 103 0]
str: future tech-blog

Appendで実装することにより記述行数が減り、可読性もぐっと上がりました。

AppendUint32、AppendUint64も基本的な利用方法は同じです。

fmt

The new functions Append, Appendf, and Appendln append formatted data to byte slices.
Go 1.19 Release Notesより引用

fmtパッケージにもAppend系のメソッドが導入されました。
こちらもbyte型のスライスを戻り値に持つメソッドです。

こちらのメソッドの導入経緯はproposalにも記載されています

fmt.Sprintf is an allocator that produces a string, but there are times when you want formatted-output, but want to write into a []byte. And you can do that with bytes.Buffer and fmt.Fprintf
…(中略)
What I want: Something like Sprintf, but that can write into a []byte, and can fail gracefully if there’s not enough space to write things.
fmt: add Append, Appendf, Appendln (#47579)より引用

フォーマットされた出力で[]byteに書き込みを行う場合、これまではbytes.Bufferとfmt.Fprintfを組み合わせて実装することができました。Appendfの導入によりこれを気軽に利用することができます。

利用方法

Sprintfと比較すると利用方法がわかりやすいと思います。
fmtにはSprintfメソッドが定義されています。これは任意の型をフォーマットし、文字列で返すメソッドです。

Appendfでは引数にbyte型スライスを持ち、Sprintfとは異なり、戻り値にbyte型スライスを持ちます。
つまり、任意の型をフォーマットをした後、引数に与えられたbyte型スライスに値を追加し、返却します。

SprintfとAppendfの実装を見たほうがわかりやすいと思いますので、以下比較を行います。

fmt.Sprintf
// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...any) string {
p := newPrinter()
p.doPrintf(format, a)
s := string(p.buf)
p.free()
return s
}
fmt.Appendf
// Appendf formats according to a format specifier, appends the result to the byte
// slice, and returns the updated slice.
func Appendf(b []byte, format string, a ...any) []byte {
p := newPrinter()
p.doPrintf(format, a)
b = append(b, p.buf...)
p.free()
return b
}

細かな点で異なりますが、2つのメソッドの本質的な差分は以下の1行のみです。

diff
-   s := string(p.buf)
+ b = append(b, p.buf...)

では実際に使用してみます。

in:=tech-blog%sでフォーマットして、空のbyte型スライスに追加します。

playground

fmt.Appendf
package main

import (
"fmt"
)

func main() {
in := "tech-blog"
out := make([]byte, 0, 100)
out = fmt.Appendf(out, "future %s", in)
fmt.Printf("raw: %v \n", out)
fmt.Printf("str: %s \n", out)
}

▼実行結果

raw: [102 117 116 117 114 101 32 116 101 99 104 45 98 108 111 103]
str: future tech-blog

空だったbyte型スライスoutに要素が追加されていますね。

Appendはフォーマットは行わず、要素の追加のみ行います。

playground

fmt.Append
package main

import (
"fmt"
)

func main() {
in := "tech-blog"
out := make([]byte, 0, 100)
out = fmt.Append(out, in)
fmt.Printf("raw: %v \n", out)
fmt.Printf("str: %s \n", out)
}

▼実行結果

raw: [116 101 99 104 45 98 108 111 103]
str: tech-blog

AppendlnはAppendした後に改行を挿入します。
先程のコードにout = fmt.Appendln(out, "future")を加えます。

playground

fmt.Appendln
package main

import (
"fmt"
)

func main() {
in := "tech-blog"
out := make([]byte, 0, 100)
out = fmt.Appendln(out, "future")
out = fmt.Append(out, in)
fmt.Printf("raw: %v \n", out)
fmt.Printf("str: %s \n", out)
}

▼実行結果

raw: [102 117 116 117 114 101 10 116 101 99 104 45 98 108 111 103]
str: future
tech-blog

期待どおり、Appendlnで追加したfutureで改行が挿入されていますね。

おわりに

今回はGo1.19でリリースされた各種ライブラリのマイナーチェンジの中でもAppend系のメソッドに注目しました。
今回のリリースには含まれていませんが、proposal: encoding: provide append-like variants #53693ではencoding/hex,encoding/base32,encoding/base64に対してAppend系のメソッドを追加しようという提案がされており、今後もencoding周りのAPIはマイナーリリースが続きそうです。

(#53693)以外にも、以下のようなProposalでAppend-likeなメソッド追加が提案されています。

普段とは異なる切り口で公式リポジトリを眺めていましたが、特定のパッケージやAPIに注目するとパッケージの役割や関数の命名に非常に慎重に議論が進められている事に気が付きます。
Goの思想や質の高いコーディングを学ぶという点で今回の連載は非常に良い体験でした。

最後までお付き合いただきありがとうございました。