Go 1.21で競プロを始めてみる
はじめに
2023年8月8日、Go 1.21.0がリリースされました。バージョン1.21からナンバリングの規則が変わり、パッチバージョンまで含めてのリリースとなります。
Go 1.21ではランタイム、コンパイラ、ライブラリなどに複数の変更がなされています。本記事では特にbuilt-inとライブラリに焦点を当てて、Goで競プロを始めるにあたって役に立つ知識を紹介していきます。
どうして今更Goで競プロ記事を書くの?
Go 1.21において、競プロで頻繁に用いられる機能に対してアップデートがありました。具体的には min
, max
のbuilt-inサポートと、スライスのソートの高速化です。
AtCoderのジャッジ環境が最新のGoのバージョンに追いつくのはまだ先ではあるものの、今後利用されてゆくことになるので紹介していきます。
max, minがbuild-inになった
Go 1.21では、新たに3つのbuild-in関数が実装されました。
-
max
: 比較可能な値を任意個受け取り、その最大値を返す -
min
: 比較可能な値を任意個受け取り、その最小値を返す -
clear
:map
,slice
を初期化する
その中でも、競プロでよく用いられるmin
, max
に焦点を当てます。
min、maxは自作しなければならない
1.21以前は、自らmax
, min
を実装する必要がありました。その上、引数として用いることのできる型が限られてしまいます。
func main() {
a := 1
b := 5
m := myMin(a, b)
fmt.Printf("%dと%dの最小値は%dです\n", a, b, m)
// 1と5の最小値は1です
}
// 自作のmin関数
// intにしか対応できない
func myMin(a int, b int) int {
if a > b {
return b
} else {
return a
}
}
genericsで複数の型に対応する
上記の実装の改善策として、1.18から導入されたgenericsを用いる方法があります。
これによって、複数の型に対して最小値を計算することができます。
func main() {
a := 1
b := 5
m := myMin(a, b)
fmt.Printf("%dと%dの最小値は %d です\n", a, b, m)
// 1と5の最小値は 1 です
}
// genericsを用いた自作のmin関数
// aとbの型が異なるとコンパイルエラーになる
func myMin[T cmp.Ordered](a T, b T) T {
if a > b {
return b
} else {
return a
}
}
今回導入されたmin, maxは、さらに高性能
今回実装されたmin
, max
は上記のmyMin
よりも優れた機能を提供してくれます。
具体的な仕様はこちら
主な特徴は以下の2点です。
- 一つの関数で、型によらず大小比較が行える
- 任意個の引数を受け取ることができる
前者は先ほどgenericsとして説明しましたが、後者は以下のように利用することができます。
引数の型や個数に囚われることなく、直感的に利用できるようになりました。
func main() {
a := 1.0
b := 5.0
c := 10.0
m := min(a, b, c) // 任意個の引数を受け取ることができる
fmt.Printf("%f, %f, %fの最小値は%fです", a, b, c, m)
// 1.000000, 5.000000, 10.000000の最小値は1.000000です
}
sliceのソートが速くなった
正確には []int
のソートが速くなりました。 これまで用いられていたsort.Ints()
関数のドキュメントに、今後はslices.Sort()
を使うよう記載があります。
Ints sorts a slice of ints in increasing order.
Note: consider using the newer slices.Sort function, which runs faster.
slicesパッケージの使い方
sort.Ints()
同様、直感的にソートを実装することができます。
また、int型に限らず、大小比較が可能な型を要素にもつスライスであればslices.Sort()
の引数に置くだけでソートできます。
func main() {
s := []int{3, 1, 4, 1, 5}
slices.Sort(s)
fmt.Println(s)
// [1, 1, 3, 4, 5]
}
実際に使ってみる
Go 1.21によって、min
, max
, sort
が平易に記述できるようになりました。
これらを利用して解くことのできる問題を実際に解いてみます。
2つの配列が与えられたとき、それぞれの配列から1要素ずつ選んだ時の差の最小値を求める問題です。
例えば、配列Aから31、配列Bから6を選択した場合は、その差が15となります。
15と18を選択した場合が最小となります。
ここまで紹介したbuild-inの関数やslicesパッケージを用いると、以下のようになります。
func main() {
// 以下を標準入力で受け取る
// A, B: 配列
// N: Aの要素数
// M: Bの要素数
slices.Sort(A) // スライスのソート
slices.Sort(B) // スライスのソート
A = append(A, 2000000000)
B = append(B, 2000000000)
i, j := 0, 0
ans := abs(A[i] - B[j])
for i < N && j < M {
if A[i] < B[j] {
i++
} else {
j++
}
ans = min(ans, abs(A[i] - B[j])) // 自身で実装せずとも利用できる
}
fmt.Println(ans)
}
func abs(a int) int {
return int(math.Abs(float64(a)))
}
平易にソートを実装でき、min
を自身で実装しなくても良くなりました。
おわりに
記事を書くことが、Goのドキュメントを熟読するいい機会になりました。
試験運用パッケージの保管場所としてのexpモジュールや、comparableの性質を知ることができました。
set(集合)に対応したパッケージの実装が待ち遠しいです。
フィシルコムのテックブログです。マーケティングSaaSを開発しています。 マイクロサービス・AWS・NextJS・Golang・GraphQLに関する発信が多めです。 カジュアル面談はこちら(ficilcom.notion.site/bbceed45c3e8471691ee4076250cd4b1)から
Discussion