Как закодировать [] руну в [] байт, используя utf8 в golang?

Так что действительно легко декодировать []byte []rune (просто приведение к string, а затем приведение к []rune работает очень хорошо, я предполагаю, что по умолчанию это utf8 и с байтами-заполнителями для инвалидов). Мой вопрос - как вы предполагаете декодировать эту []rune обратно в []byte в форме utf8?

Я что-то пропустил или мне пришлось вручную вызывать EncodeRune для каждой руны в моей []rune? Конечно, есть кодировщик, которому я могу просто передать Writer.

Ответ 1

Вы можете просто преобразовать фрагмент руны ([]rune) в string которую вы можете преобразовать обратно в []byte.

Пример:

rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
bs := []byte(string(rs))

fmt.Printf("%s\n", bs)
fmt.Println(string(bs))

Вывод (попробуйте на Go Playground):

Hello 世界
Hello 世界

В спецификации Go: Conversions этот случай явно упоминается: преобразования в и из строкового типа, точка № 3:

Преобразование фрагмента рун в строковый тип дает строку, которая является объединением отдельных значений рун, преобразованных в строки.

Обратите внимание, что приведенное выше решение - хотя и может быть самым простым - может быть не самым эффективным. И причина в том, что он сначала создает string значение, которое будет содержать "копию" рун в кодированной форме UTF-8, а затем копирует вспомогательный фрагмент строки в результирующий фрагмент байта (копия должна быть сделана, потому что string значения являются неизменяемыми, и если результирующий срез будет совместно использовать данные со string, мы сможем изменить содержимое string, подробнее см. golang: [] byte (string) vs [] byte (* string) и Неизменная строка и адрес указателя).

Обратите внимание, что умный компилятор может обнаружить, что на промежуточное string значение нельзя ссылаться, и, таким образом, исключить одну из копий.

Мы можем повысить производительность, выделив один байт и закодировав в него руны по одной. И мы сделали. Чтобы легко сделать это, мы можем вызвать пакет unicode/utf8 для нашей помощи:

rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
bs := make([]byte, len(rs)*utf8.UTFMax)

count := 0
for _, r := range rs {
    count += utf8.EncodeRune(bs[count:], r)
}
bs = bs[:count]

fmt.Printf("%s\n", bs)
fmt.Println(string(bs))

Вывод вышеописанного одинаков. Попробуйте это на игровой площадке Go.

Обратите внимание, что для создания результирующего среза нам нужно было угадать, насколько большим будет результирующий срез. Мы использовали максимальную оценку, которая представляет собой число рун, умноженное на максимальное количество байтов, в которые может быть закодирована руна (utf8.UTFMax). В большинстве случаев это будет больше, чем нужно.

Мы можем создать третью версию, в которой сначала рассчитаем точный необходимый размер. Для этого мы можем использовать utf8.RuneLen(). Выигрыш будет состоять в том, что мы не будем "тратить" память, и нам не придется делать окончательное нарезание (bs = bs[:count]).

Давай сравним выступления. 3 функции (3 варианта) для сравнения:

func runesToUTF8(rs []rune) []byte {
    return []byte(string(rs))
}

func runesToUTF8Manual(rs []rune) []byte {
    bs := make([]byte, len(rs)*utf8.UTFMax)

    count := 0
    for _, r := range rs {
        count += utf8.EncodeRune(bs[count:], r)
    }

    return bs[:count]
}

func runesToUTF8Manual2(rs []rune) []byte {
    size := 0
    for _, r := range rs {
        size += utf8.RuneLen(r)
    }

    bs := make([]byte, size)

    count := 0
    for _, r := range rs {
        count += utf8.EncodeRune(bs[count:], r)
    }

    return bs
}

И код бенчмаркинга:

var rs = []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}

func BenchmarkFirst(b *testing.B) {
    for i := 0; i < b.N; i++ {
        runesToUTF8(rs)
    }
}

func BenchmarkSecond(b *testing.B) {
    for i := 0; i < b.N; i++ {
        runesToUTF8Manual(rs)
    }
}

func BenchmarkThird(b *testing.B) {
    for i := 0; i < b.N; i++ {
        runesToUTF8Manual2(rs)
    }
}

И результаты:

BenchmarkFirst-4        20000000                95.8 ns/op
BenchmarkSecond-4       20000000                84.4 ns/op
BenchmarkThird-4        20000000                81.2 ns/op

Как и предполагалось, вторая версия быстрее, а третья - самая быстрая, хотя прирост производительности невелик. В общем, первое, простейшее решение является предпочтительным, но если оно находится в какой-то критической части вашего приложения (и выполняется много-много раз), третья версия может стоить того, чтобы ее использовать.