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