フューチャー技術ブログ

Go1.23 リリース連載 slices/maps(+ unique)

はじめに

Go 1.23リリース連載 の4本目です。

Go 1.23の目玉機能でもあるイテレータの導入に合わせて、slices パッケージと maps パッケージにも新たな関数が追加されました。
本記事では新たに追加された関数をサンプルコードとともに紹介します。

イテレータの詳細については、連載3本目の記事「Go 1.23リリース連載 range over funcとiterパッケージ」を参照してください。

関連する Issues

今回のアップデートは、range over function(#61405)の追加に伴うものとなります。iter パッケージの追加(#61897)をはじめとする標準ライブラリの一連のアップデートの内の1つとして位置付けられています。

slicesmaps に関する Issue は次の2つです。

実行環境

Goのバージョンは go1.23rc2 を使用しています。

$ go version
go version go1.23rc2 linux/arm64

サンプルコードはこちらのリポジトリで公開しています。
https://github.com/rhumie/tech-blog-go-1.23-feature

slices

関数名 説明
All スライスのインデックスと要素に対するイテレータを返却します
Values スライスの要素に対するイテレータを返却します
Backward スライスの要素に対する逆方向のイテレータを返却します
Collect イテレータからスライスを生成し、返却します
AppendSeq イテレータからスライスに対して要素を追加します
Sorted イテレータからスライスを生成し、ソートした上でスライスを返却します
SortedFunc イテレータからスライスを生成し、指定された関数をもとにソートした上でスライスを返却します
SortedStableFunc イテレータからスライスを生成し、指定された関数をもとにソート(安定ソート)した上でスライスを返却します
Chunk スライスを指定された要素数のサブスライスのイテレータとして返却します

All

スライスのインデックスと要素に対するイテレータを返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
values := []string{"a", "b", "c"}

for i, s := range slices.All(values) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

0: a
1: b
2: c

Values

スライスの要素に対するイテレータを返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
values := []string{"a", "b", "c"}

for s := range slices.Values(values) {
fmt.Printf("%s\n", s)
}
}

▼ 実行結果

a
b
c

Backward

スライスの要素に対する逆方向のイテレータを返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
values := []string{"a", "b", "c"}

for i, s := range slices.Backward(values) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

2: c
1: b
0: a

Collect

イテレータからスライスを生成し、返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
it := func(yield func(string) bool) {
for _, s := range []string{"a", "b", "c"} {
if !yield(s) {
return
}
}
}

for i, s := range slices.Collect(it) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

0: a
1: b
2: c

AppendSeq

イテレータからスライスに対して要素を追加します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
values := []string{"a", "b", "c"}

it := func(yield func(string) bool) {
for _, s := range []string{"d", "e", "f"} {
if !yield(s) {
return
}
}
}

for i, s := range slices.AppendSeq(values, it) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

0: a
1: b
2: c
3: d
4: e
5: f

Sorted

イテレータからスライスを生成し、ソートした上でスライスを返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
it := func(yield func(string) bool) {
for _, s := range []string{"b", "c", "a"} {
if !yield(s) {
return
}
}
}

for i, s := range slices.Sorted(it) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

0: a
1: b
2: c

SortedFunc

イテレータからスライスを生成し、指定された関数をもとにソートした上でスライスを返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
"strings"
)

func main() {
type Person struct {
Name string
Age int
}

it := func(yield func(Person) bool) {
for _, s := range []Person{
{"Gopher", 13},
{"Alice", 20},
{"Bob", 24},
{"Alice", 55},
} {
if !yield(s) {
return
}
}
}

for i, v := range slices.SortedFunc(it, func(s1, s2 Person) int {
return strings.Compare(s1.Name, s2.Name)
}) {
fmt.Printf("%d: %v\n", i, v)
}
}

▼ 実行結果

0: {Alice 20}
1: {Alice 55}
2: {Bob 24}
3: {Gopher 13}

※ 次に説明する SortedStableFunc と異なり、安定ソートではないため、Aliceの順番は変わる可能性があります。

SortedStableFunc

イテレータからスライスを生成し、指定された関数をもとにソートした上でスライスを返却します。
SortedFunc と異なり、安定ソート(同等なデータのソート前の順序が、ソート後も保存される)を行います。

▼ サンプルコード

package main

import (
"fmt"
"slices"
"strings"
)

func main() {
type Person struct {
Name string
Age int
}

it := func(yield func(Person) bool) {
for _, s := range []Person{
{"Gopher", 13},
{"Alice", 20},
{"Bob", 24},
{"Alice", 55},
} {
if !yield(s) {
return
}
}
}

for i, v := range slices.SortedFunc(it, func(s1, s2 Person) int {
return strings.Compare(s1.Name, s2.Name)
}) {
fmt.Printf("%d: %v\n", i, v)
}
}

▼ 実行結果

0: {Alice 20}
1: {Alice 55}
2: {Bob 24}
3: {Gopher 13}

SortedFunc とは異なり、Aliceの順番は保証されます。

Chunk

スライスを指定された要素数のサブスライスのイテレータとして返却します。

▼ サンプルコード

package main

import (
"fmt"
"slices"
)

func main() {
values := []string{"a", "b", "c", "d", "e"}

for s := range slices.Chunk(values, 2) {
fmt.Printf("%v\n", s)
}
}

▼ 実行結果

[a b]
[c d]
[e]

maps

Go 1.21にて正式導入が見送られていた(#61538KeysValues が、イテレータの導入に伴い、正式に標準ライブラリに加わりました。

関数名 説明
All マップのキーとバリューに対するイテレータを返却します
Keys マップのキーに対するイテレータを返却します
Values マップのバリューに対するイテレータを返却します
Insert イテレータからマップに対してキー・バリューのペアを追加します
Collect イテレータからマップを生成し、返却します

All

マップのキーとバリューに対するイテレータを返却します。
イテレータの順序は指定されず、呼び出しごとに異なる可能性があります。

▼ サンプルコード

package main

import (
"fmt"
"maps"
)

func main() {
m := map[int]string{
1: "one",
10: "ten",
100: "hundred",
}

for i, s := range maps.All(m) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

10: ten
100: hundred
1: one

※順序は実行するたびに変わります。

Keys

マップのキーに対するイテレータを返却します。

▼ サンプルコード

package main

import (
"fmt"
"maps"
)

func main() {
m := map[int]string{
1: "one",
10: "ten",
100: "hundred",
}

for i := range maps.Keys(m) {
fmt.Printf("%d\n", i)
}
}

▼ 実行結果

100
1
10

※順序は実行するたびに変わります。

Values

マップのバリューに対するイテレータを返却します。

▼ サンプルコード

package main

import (
"fmt"
"maps"
)

func main() {
m := map[int]string{
1: "one",
10: "ten",
100: "hundred",
}

for s := range maps.Values(m) {
fmt.Printf("%s\n", s)
}
}

▼ 実行結果

ten
hundred
one

※順序は実行するたびに変わります。

Insert

イテレータからマップに対してキー・バリューのペアを追加します。
マップ内に既にキーが存在する場合は上書きされます。

▼ サンプルコード

package main

import (
"fmt"
"maps"
)

func main() {

m := map[int]string{
1: "one",
10: "ten",
100: "hundred",
}

it := func(yield func(int, string) bool) {
for i, s := range map[int]string{
10: "zehn",
11: "elf",
} {
if !yield(i, s) {
return
}
}
}

maps.Insert(m, it)

for i, s := range m {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

10: zehn
100: hundred
11: elf
1: one

※順序は実行するたびに変わります。

Collect

イテレータからマップを生成し、返却します。

▼ サンプルコード

package main

import (
"fmt"
"maps"
)

func main() {
it := func(yield func(int, string) bool) {
for i, s := range map[int]string{
1: "one",
10: "ten",
100: "hundred",
} {
if !yield(i, s) {
return
}
}
}

for i, s := range maps.Collect(it) {
fmt.Printf("%d: %s\n", i, s)
}
}

▼ 実行結果

100: hundred
1: one
10: ten

※順序は実行するたびに変わります。

unique (おまけ)

これまで紹介した slicesmaps とは毛色が異なりますが、Go1.23で新たに追加された unique パッケージ(#62483)についても紹介します。
これは、サードパーティのパッケージの go4org/intern にインスパイアされたものであり、インターン化(interning)といわれる機構によりメモリを節約できます。1

インターン化という言葉を聞き馴染みない方もいるかもしれませんが、Wikipediaでインターン化を調べると「新しいオブジェクトを作成する代わりに、同じ値のオブジェクトを再利用することである(意訳)」と説明されています。

interning is re-using objects of equal value on-demand instead of creating new objects

実際にサンプルコードを見てみましょう。

▼ サンプルコード

package main

import (
"fmt"
"strings"
"unique"
"unsafe"
)

func Canonicalize(ss []string) {
for i, s := range ss {
h := unique.Make(s)
ss[i] = h.Value()
}
}

func main() {

ss := []string{
strings.Repeat("x", 6),
strings.Repeat("x", 6),
strings.Repeat("x", 6),
}

fmt.Println("Before:")
for _, s := range ss {
fmt.Println(unsafe.StringData(s))
}

Canonicalize(ss)

fmt.Println("After:")
for _, s := range ss {
fmt.Println(unsafe.StringData(s))
}
}

▼ 実行結果

Before:
0x400000e0b0
0x400000e0b8
0x400000e0c0
After:
0x400000e0e0
0x400000e0e0
0x400000e0e0

unique パッケージの MakeValue を利用して、文字列のインターン化を行なってますが、実行前は string を構成する基底バイト列(underlying bytes)のメモリアドレスが異なるのに対し、実行後は全て同じになっていることが確認できます。

特定のシナリオにおいてプログラムのパフォーマンスの最適化が必要な場合に有効となります。

おわりに

slicesmaps 、そして unique を紹介しました。
range over func とイテレータの登場に合わせて、非常に注目度の高いアップデートではないかと思います。

次回は、辻さんによる text/template の紹介です。


  1. 1.他の言語を見てみると、JavaにおいてはString.Intern()、Pythonにおいてはsys.intern() という形で同様の機能が提供されています。