Go-string

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容