Go-string

aside section ._1OhGeD · · 1156 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

本文将讲解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” 提问留言,或者加入知识星球“链人成长” 与我深度链接~


有疑问加站长微信联系(非本文作者)

本文来自:简书

感谢作者:aside section ._1OhGeD

查看原文:Go-string

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1156 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传