String Handling in Go Using Rune

Go 字符串处理使用 Rune

前言

在 Go 字符串中索引位置 n 时,为什么没有得到第 n 个字符?
func main() {
foo := "ABC"
for _, v := range foo {
fmt.Println(v) // 65, 66, 67
}
}

相较于其他编程语言一串文字在遍历时会预期拿到单一个字符,在 Go 会拿到「Rune」;如果直接通过索引取得 string 内容会拿到 byte:

func main() {
s := "Hello世界"
fmt.Println(len(s)) // 11 byte
fmt.Println(s[0]) // 72 (H 的 byte 值)
fmt.Println(s[5]) // 228 (世 的第一个 byte)
}

啥是 Rune?

要创建一个 Rune 可以通过 '' 单引号定义:

r := 'A'

Rune 是内建的类型,实际上是 int32 的别名(所有方面都等价),设计用来表示一个 Unicode 码点(code point),使得 Go 能够正确处理各种语言的字符。

String vs Rune vs Byte

  • Byte(字节)
    • uint8 的别名,代表一个 8 位的值(0-255)
    • 构成字符串的基本单位
  • String(字符串)
    • 由一系列 byte 组成的不可变序列
    • 可以包含任何有效的 UTF-8 字符
  • Rune(符文)
    • int32 的别名
    • 设计用于存放 Unicode codepoint

实际案例

转换为 Rune Slice

func main() {
s := "Hello世界"
runes := []rune(s)
fmt.Println(runes[5]) // 19990 (世)
fmt.Printf("%c\n", runes[5]) // 世
fmt.Println("字符数量:", len(runes)) // 7
}

使用 Range 遍历

func main() {
s := "Hello世界"
for i, r := range s {
fmt.Printf("索引: %d, 字符: %c, Unicode: U+%04X\n", i, r, r)
}
// 索引: 0, 字符: H, Unicode: U+0048
// 索引: 1, 字符: e, Unicode: U+0065
// 索引: 2, 字符: l, Unicode: U+006C
// 索引: 3, 字符: l, Unicode: U+006C
// 索引: 4, 字符: o, Unicode: U+006F
// 索引: 5, 字符: 世, Unicode: U+4E16
// 索引: 8, 字符: 界, Unicode: U+754C
}

Indexed capitalization🔗

给定一个由「小写字母」组成的字符串和「一个整数索引」数组,将指定索引处的所有字母大写。如果索引超出字符串范围,则忽略该索引。

Terminal window
"abcdef", [1,2,5] ==> "aBCdeF"
"abcdef", [1,2,5,100] ==> "aBCdeF" // There is no index 100.

初步我的想法是创建空的 []rune 并遍历字符串 st 判断当前字符是否存在 arr 当中,如果是则推入大写反之小写。所以会是 O(n × m)。

import "unicode"
func Capitalize(st string, arr []int) string {
result := []rune{}
for i, c := range st {
if contains(arr, i) {
result = append(result, unicode.ToUpper(c))
} else {
result = append(result, c)
}
}
return string(result)
}
func contains(arr []int, val int) bool {
for _, v := range arr {
if v == val {
return true
}
}
return false
}

额外留意:Byte Index 不是 Char Index

假设题目没有输入为英文小写的限制。

综合以上对 string 处理的知识,可以发现 i 实际上是 string 背后的 byte,而一个字符可能由多个 byte 构成导致出错,应该以字符 index 为基准而非 byte index:

func CapitalizeByCharIndex(st string, arr []int) string {
result := []rune{}
charIndex := 0 // 手动维护字符索引
for _, c := range st { // 不用 byte 索引 i
if contains(arr, charIndex) {
result = append(result, unicode.ToUpper(c))
} else {
result = append(result, c)
}
charIndex++ // 每个字符递增
}
return string(result)
}

换个思路:从要转大写的数组下手

st 转换为 []rune 背后实际上是一次遍历转换,再通过遍历 arr 覆写上大写的字符 ,整个流程更直白是 O(n + m)。

func CapitalizeByChar(st string, arr []int) string {
runes := []rune(st)
for _, idx := range arr {
if idx < len(runes) {
runes[idx] = unicode.ToUpper(runes[idx])
}
}
return string(runes)
}

延伸阅读