Golang string

字符串的本质

Golang中的string,实际是一个不可变的字节序列,每个字节都是UTF-8编码的文本。 虽然每一个字符串都有长度,但是这个长度和数组不一样,不同长度的字符串还是同样的string类型。

我们来看一下字符串的结构,在reflect.StringHeader中,我们可以看到定义:

type StringHeader struct {
    Data uintprt
    Len  int
}

从这里我们可以看到,string其实是一个结构,Data持有的是底层字节数组,Len就是字符串的长度了。 因此对string的复制,也就是StringHeader结构体的复制,并不涉及到底层字节数组的复制,所以没有什么消耗。 另外,相同的字符串面值的常量,对应的一般都是同一个字节数组。 string虽然底层不是切片,但是支持切片的操作。

字符串的使用

遍历字符串

遍历字符串,是使用range来进行的,虽然for也可以遍历字符串的内容,但是会有问题:

func main() {
    s := "123"
    for i := 0; i < len(s); i++ {
        fmt.Println(s[i])
    }
}

output:

49
50
51

等会儿,这个有点和想象的不一样啊,为什么不是123的输出呢? 这是因为Golang是UTF-8编码,123对于的码的值就是这些数字。 我们可以转换成字符再打印。但是还会有其他问题

func main() {
    s := "hi,世界"
    for i := 0; i < len(s); i++ {
        fmt.Printf("%c\n", s[i], s[i])
    }
}

output:

h
i
,
ä
¸
–
ç
•
Œ

这里又出现乱码了,这个是因为UTF-8在编码的时候,是变长编码的,对于ASCII是采用一个byte来编码的, 而对于其他Unicode字符来说则是两个字节的编码。我们通过for来遍历,其实还是在遍历底层的字节序列。

对于这个,我们当然可以转成Golang中的[]rune类型。(话说用rune,符文,这个词,对于西方世界还是挺形象的)。 []rune其实是[]int32的别名,而不是重新定义的新类型。

func main() {
    s := "hi,世界"
    r := []rune(s)
    for i := 0; i < len(r); i++ {
        fmt.Printf("%c\n", r[i])
    }
}

output:

h
i
,
世
界

这次就对了。

但是一般情况下,我们不需要这么麻烦,是可以用range来遍历的:

func main() {
    s := "hi,世界"
    for _, v := range s {
        fmt.Printf("%c\n", v)
    }
}

output:

h
i
,
世
界

这是因为Golang中的range针对string做了特殊的处理。

截取字符串

截取其中一部分字符串是非常容易的,因为字符串是支持切片操作的。只要substr := str[m:n]即可。

改变中某个字符

因为字符串是不支持直接修改的,所以必须曲线救国。 我们已经知道,字符串是UTF-8编码的了,那么对于只有ASCII的包含非ASCII字符的两种string,可以有不同的改法。

如果字符串只包含ASCII字符,那么可以将string转换成[]byte,然后再来修改。

func main() {
    s := "123"
    bs := []byte(s)
    bs[1] = 'a'
    s = string(bs)
    fmt.Println(s)
    // “1a3”
}

如果字符串中包含Unicode字符,那么需要将string转换成[]rune,然后再进行修改。

func main() {
    s := "hi,世界"
    rs := []rune(s)
    rs[3] = 'w'
    s = string(rs)
    fmt.Println(s)
    // “hi,w界”
}

计算长度

虽然string支持len操作,但是这个取出来的是底层byte数组的长度。 所以如果字符串全部都是ASCII,可以使用len来获取长度,如果包含unicode,则需要使用utf8.RuneCountInString(str)

func main() {
    s := "123"
    println(len(s))  // 3
    s = "hi,世界"
    println(len(s))  // 9
    println(utf8.RuneCountInString(s)) // 5
}

字符串的拼接

字符串的拼接可以有以下几种方式:

  1. 最简单的,使用+
  2. 最高效的,使用字节拼接
  3. 使用strings包的方法

    func main() {
    s1 := "hi"
    s2 := "世界"
    
    // 1
    println(s1 + s2)
    
    // 2
    var buf bytes.Buffer
    buf.WriteString(s1)
    buf.WriteString(s2)
    println(buf.String())
    
    // 3
    str := strings.Join([]string{s1, s2}, "")
    println(str)
    }
    

字符串与数字的转换

在实际的使用过程中,我们经常会需要将字符串与数字进行转换,可以通过strconv提供的各种函数来实现。

源类型 目标类型 方式
string int string, _ := strconv.Atoi(“1”)
int string int := strconv.Itoa(1)
string int64 int64, _ := strconv.ParseInt(“1”, 10, 64)
int64 string string := strconv.FormatInt(1, 64)