フューチャー技術ブログ

Go 1.17からの負のruneの扱い

こんにちは、TIGの玉木です。Go 1.17連載の4記事目です。

この記事ではGo 1.17で更新があった負のruneの扱いについてです。更新自体は簡単なもので、この記事ではruneの説明から行います。

この記事を書いたきっかけ

Go 1.17のRelsease Notes1を眺めていたらいくつかの箇所で negative rune valuesという記述があり、runeってなんだ? と気になったことがきっかけです。

runeについて

こちらのseihmdさんのQiita記事がとてもわかりやすかったです。詳細はそちらの記事に任せて、この記事では簡単な紹介に留めます。

コンピュータは文字を0と1のビットで表現します。例えば「”あ”, ”い”, “う”, “え”, “お”」の5種類の文字しかなければ、それぞれ「”000”, “001”, “010”, “011”, “100”」のように0と1で文字を表現できます。このように文字に非負整数値を対応付け、コンピュータに利用できるように変換することを、文字符号化と呼びます。この文字符号化のうちの1つがUnicodeであり、対応する非負整数値をコードポイントと呼びます。例えば”あ”という文字のコードポイントは3042(16進数表記)となります。

Goではこのコードポイントをより短い用語としてruneを導入したようです2。コードポイントとruneは全く同義です。

報告されていた問題

https://github.com/golang/go/issues/43254

上記issueでは負のruneが来た場合、他の異常なruneと同じように振る舞うべきだがそうなっていないと報告しています。issueを報告している方がGo Playgroundで共有しているコードが以下になります。

package main

import (
"bytes"
"fmt"
"unicode"
"unicode/utf8"
)

func main() {
// unicode.Is goes checks rangeTab.R16 with negative values.
// -2147483583 = 0x80000041
fmt.Printf("unicode.IsPrint(-2147483583) = %t\n", unicode.IsPrint(-2147483583))

// bytes.Buffer.WriteRune runs the single-byte codepath with
// negative values, even writing invalid UTF-8.
var b bytes.Buffer
b.WriteRune(-2147483583) // 0x80000041
b.WriteRune(-2147483393) // 0x800000ff
fmt.Printf("b.String() = %q\n", b.String())
fmt.Printf("utf8.ValidString(b.String()) = %t\n", utf8.ValidString(b.String()))
}
出力(Go1.16以前)
unicode.IsPrint(-2147483583) = true
b.String() = "A\xff"
utf8.ValidString(b.String()) = false

unicode.IsPrint() が負の整数を入れているにも関わらず、trueになっています。runeは非負整数しか取り得ないため、falseを返すべきです。

また、Builder.WriteRuneで負の整数を入れているにも関わらず、b.String()に文字が入っています。Unicodeではこのような場合、REPLACEMENT CHARACTERと呼ばれる文字�(rune: U+FFFD)に変換するのが一般的です3。また、異常な文字が入ってしまっているため、UTF-8.ValidString(b.String())がfalseになっています。

今回の更新について

以下Go 1.17 Relsease Notesからの本記事に関係がある箇所の引用です。

bufio
The Writer.WriteRune method now writes the replacement character U+FFFD for negative rune values, as it does for other invalid runes.

bytes
The Buffer.WriteRune method now writes the replacement character U+FFFD for negative rune values, as it does for other invalid runes.

strings
The Builder.WriteRune method now writes the replacement character U+FFFD for negative rune values, as it does for other invalid runes.

unicode
The Is, IsGraphic, IsLetter, IsLower, IsMark, IsNumber, IsPrint, IsPunct, IsSpace, IsSymbol, and IsUpper functions now return false on negative rune values, as they do for other invalid runes.

  • bufio
  • bytes
  • strings

上記パッケージでは、負のruneを他の無効なruneと同じくU+FFFDに置き換えるように修正されています。

  • Unicode

負のruneの場合、該当する関数ではfalseを返すように修正されています。

Go 1.17では、先程の問題を再現するコードは以下のような出力になります。

出力(Go1.17)
unicode.IsPrint(-2147483583) = false
b.String() = "��"
utf8.ValidString(b.String()) = true

unicode.IsPrint() に負の整数を入れるとfalseを返すようになります。Builder.WriteRuneで負の整数を入れているため、�に置き換えられています。�が代わりに入るようになったため、UTF-8.ValidString()がtrueを返します。

まとめ

Go 1.17では以下のように負のruneの扱いが修正されました。

  • bufio, bytes, stringsパッケージでのWriteRuneメソッドにおいて、負のruneをU+FFFDに置き換える
  • Unicodeパッケージでのいくつかの関数において、負のruneが来た場合falseを返す

あまりコードを書いていて気にする箇所ではないかもしれませんが、この記事が参考になれば幸いです。