Go-string

作者: 链人成长chainerup | 来源:发表于2019-09-24 22:39 被阅读0次

    本文将讲解Go中字符串相关的知识。

    1 编码知识

    在讲解String之前,我们先讲解一下编码。因为在讲解string过程中,会用到编码知识。

    1.1 字符集

    字符集规定了某个文字对应的二进制数字存放方式(编码)以及某串二进制数字代表了哪个文字(解码)的转换关系。比如我们常见的Unicode字符集。相当于定义了一套标准。

    1.2 编码

    编码字符集:用一个编码值(code point)来表示一个字符在字库中的位置。
    字符编码:编码字符集和实际存储数据之间的转换关系。比如我们常见的utf-8编码。相当于标准的一个实现。

    1.3 乱码

    我们在开发中,经常遇到的一个棘手的问题就是乱码。为什么乱码呢?就是因为编、解码采用的字符编码不一致。就好比同样的一串字符,在英语跟俄语中的含义可能不一样。

    2 string的结构

    type StringHeader struct {
        Data uintptr
        Len  int
    }
    

    是不是跟slice很像?
    就是一个类型,加一个长度。比slice少了一个cap的定义。这是因为string是一个不可变的,对原string的任何操作操作,都会产生一个新的string。

    3 长度

    这部分我们看一下字符串的长度问题。
    举个例子:

    func main() {
      s := "hello 中国"
      fmt.Println(len(s)) // 12
    }
    

    结论是12,而不是8。为什么呢?

     // The len built-in function returns the length of v, according to its type:
    func len(v Type) int
    

    这是因为len统计string的长度,是按照字节进行统计的。string在Go中默认采用Unicode编码,一个汉字占3个字节,所以就是12.

    如果我们想得到8怎么办呢?这儿就需要使用rune了。
    先来个例子,再讲原理:

    func main() {
        str := "hello 中国"
        fmt.Println(len(str)) // 12
        str2 := []rune(str)
        fmt.Println(len(str2)) //8
    }
    

    这是为什么呢?先看下rune的定义:

    // rune is an alias for int32 and is equivalent to int32 in all ways. It is
    // used, by convention, to distinguish character values from integer values.
    type rune rune
    

    rune是int32的别名,可以存放4个字节,所以汉字虽然占了3个字节,但是也只是占用一个rune。

    4 循环

    4.1 经典循环 for i :=0; i< len(str); i++

    先来个例子:

          str := "hello 中国"
        for i := 0; i < len(str); i++ {
            fmt.Println(i, str[i], string(str[i])) 
        }
    

    结论:

    0 104 h
    1 101 e
    2 108 l
    3 108 l
    4 111 o
    5 32  
    6 228 ä
    7 184 ¸
    8 173 ­
    9 229 å
    10 155 �
    11 189 ½
    

    发现有乱码,因为如上文所讲,汉字占3个字节,如果按照字节遍历字符串,读到汉字时,每次读取一个字节,就出现乱码呢。
    那怎么办呢?看下面一节。

    4.2 for range

    先对上面的case进行一下改造:

        str := "hello 中国"
        for k, v := range str {
            fmt.Println(k, v, string(v))
        }
    

    结果呢?

    0 104 h
    1 101 e
    2 108 l
    3 108 l
    4 111 o
    5 32  
    6 20013 中
    9 22269 国
    

    这是为什么呢?
    如之前我的文章 Go-for range 一文 所讲,for range 在遍历string 时,是按照rune进行处理的。
    原理可以参考我的文章。

    5 类型转换 rune-string-[]byte

    在讲解具体的转换之前,我们必须强调一下:无论哪种类型转换到另外一种,都需要内存拷贝,这是会损耗性能的

    5.1 rune与[]byte转换

    下面给一个例子,例子中三个方法,每一个都是基于前一个的优化:

    func TestRune2String2Byte_func1(t *testing.T)  {
        fmt.Println("最简单,但是性能差,因为涉及rune 2 string ,然后 string 2 byte, 两次内存拷贝,所以性能差")
        rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
        bs := []byte(string(rs))
    
        fmt.Printf("%s\n", bs)
        fmt.Println(string(bs))
    }
    
    func TestRune2String2Byte_func2(t *testing.T)  {
        fmt.Println("不涉及string的两次转换,但是bs 分配到内存太大")
        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))
    }
    
    func TestRune2String2Byte_func3(t *testing.T)  {
        fmt.Println("先统计rune的大小,然后分配指定大小的slice。 接着才拷贝到slice中")
        rs := []rune{'H', 'e', 'l', 'l', 'o', ' ', '世', '界'}
        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)
        }
    
        fmt.Printf("%s\n", bs)
        fmt.Println(string(bs))
    }
    

    方法一(TestRune2String2Byte_func1):很简单地就可以将rune转换成byte,但是中间涉及到两次内存拷贝,所以性能差一些。
    方法二(TestRune2String2Byte_func2):直接将rune转换成byte[],但是byte[]的内存占用太大;
    方法三(TestRune2String2Byte_func3):在方法二的基础上进行了优化,对byte[]的大小进行了限制,根据实际占用分配。

    5.2 string与rune 、string与[]byte转换

    下面是string 与 rune, string 与[]byte 转换的例子:

    str := "hello go gogo"
    
    //string 转[]byte
    b := []byte(str)
    
    //[]byte转string
    str = string(b)
    
    //string 转 rune
    r := []rune(str)
    
    //rune 转 string
    str = string(r)
    

    6 常见API举例

    这部分罗列了string常见的一些用法,其内部实现还是比较经典的,等有时间需要拜读一下内部实现:

    func TestStringApi(t *testing.T)  {
        s := "Hello string sasasasaS"
        // 以某个字符开头
        strings.HasPrefix(s, "test")
        // 以某个字符结尾
        strings.HasSuffix(s, "test")
        // TODO e 在s中第一次出现的位置, 没有则 -1
        strings.Index(s, "e")
        // TODO e 在s中最后出现的位置,如果没有,返回-1
        strings.LastIndex(s, "e")
        // 字符串替换
        oldStr := "s"
        newStr := "a"
        // n 为最多替换几个
        fmt.Println(strings.Replace(s, oldStr, newStr, 2)) // Hello atring aasasasaS
        // 字符串计数
        fmt.Println(strings.Count(s, "sa")) // 4
    
        // 重复count次str
        fmt.Println(strings.Repeat(s, 2)) // Hello string sasasasaSHello string sasasasaS
        // 转为小写
        fmt.Println(strings.ToLower(s)) // hello string sasasasas
        // 转为大写
        fmt.Println(strings.ToUpper(s)) // HELLO STRING SASASASAS
        // 去掉字符串收尾空白字符
        s1 := " Hello string sasasasaS "
        fmt.Println("原字符串长度: ", len(s1)) // 原字符串长度:  24
        fmt.Println("去掉收尾空白字符长度", len(strings.TrimSpace(s1))) // 去掉收尾空白字符长度 22
        // 去掉字符串首尾cut字符
        s2 := "SHello string sasasasaS"
        fmt.Println(strings.Trim(s2, "S")) // Hello string sasasasa
        fmt.Println(strings.TrimLeft(s2, "S")) // Hello string sasasasaS
        fmt.Println(strings.TrimRight(s2, "S")) // SHello string sasasasa
    
        // 返回字符串 空格分隔的所有子串的slice
        s3 := "SHello string sasasasaS"
        fmt.Println(strings.Fields(s3)) // [SHello string sasasasaS]
    
        // 返回str split 分隔的所有子串的slice
        s4 := "SHello string sasasasaS"
        fmt.Println(strings.Split(s4, "s")) // [SHello  tring  a a a aS]
    
        // 用sep把s1中的所有元素链接起来
        sArr := []string{"zp", "chris", "lmm"}
        fmt.Println(strings.Join(sArr, "---")) // zp---chris---lmm
    
        // 把一个整数转换为字符串
        i := 1
        fmt.Println(strconv.Itoa(i))
        // 把字符串转换为整数
        strTest := "a"
        fmt.Println(strconv.Atoi(strTest)) // 0 strconv.Atoi: parsing "a": invalid syntax
        fmt.Println(strconv.Atoi("1")) // 1 <nil>
    }
    

    7 总结

    本文从编码入手,讲解了string的结构、长度、循环,以及各种类型之间的转换。最后罗列了string常见的api。

    8 参考文献

    十分钟搞清字符集和字符编码 http://cenalulu.github.io/linux/character-encoding/
    Golang rune []byte string 的相互转换https://blog.csdn.net/dengming0922/article/details/80883574
    【golang】浅析rune,byte https://blog.csdn.net/HaoDaWang/article/details/79971395

    9 其他

    本文是《循序渐进go语言》的第十二篇-《Go-string》。
    如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

    相关文章

      网友评论

        本文标题:Go-string

        本文链接:https://www.haomeiwen.com/subject/qdgbuctx.html