乐趣区

关于go:13-GolangGo语言快速入门字符串

  Go 语言字符串的用法还是比较简单的,罕用也就是字符串相加,字符串与 byte 切片、rune 切片相互转换,字符串输入等等操作。那有什么可学的呢?其实还是有一些细节须要关注,比方字符串 ” 只读 ” 个性,字符串编码等等。

基本操作

  字符串只读?是的,就是你想的那样,只读就是不能批改的意思。那上面程序怎么解释呢?

package main

import "fmt"

func main() {
    str := "hello"
    str += "world"
    fmt.Println(str)   //hello world
}

  看到了吧,我的确扭转了字符串 str 的值。是的,字符串 str 的确扭转了,而字符串的确也是只读的;这里可能存在一些歧义,筹备的说,应该是字符串变量 str 指向了新的字符串。字符串 ”hello” 并没有扭转,只是创立了一个新的字符串 ”hello world”,同时让字符串变量 str 指向这个新的字符串。还有一个办法验证这个说法:

go tool compile -S -N -l test.go

go.string."hello" SRODATA dupok size=5
    0x0000 68 65 6c 6c 6f                                   hello
go.string."world" SRODATA dupok size=6
    0x0000 20 77 6f 72 6c 64                                 world

  go.string.”hello” 所属内存区域是 SRODATA,RO 就是 read only 只读的意思。再举一个事例:字符串不能依照索引操作,如果将将字符串转换为 byte 切片,按理说 byte 切片与字符串底层数据应该共用,那么批改该 byte 切片,字符串也应该同步扭转。

package main

import "fmt"

func main() {
    str := "hello world"
    // 字符串转化为 byte 切片,批改切片元素
    b := []byte(str)
    b[0] = 69

    fmt.Println(str)          //hello world
    for _, c := range b {fmt.Printf("%c", c)   //Eello world
    }
}

  byte 切片的确被批改了,而字符串变量 str 却没有扭转,为什么呢?因为字符串是只读的,所以在 []byte(str) 强制类型转化时,会执行了数据的拷贝,防止批改 byte 切片影响原字符串。

  最初在应用字符串时,还须要留神一个问题:len 用于获取字符串长度,纯英文字符串一切正常,然而当字符串中蕴含中文时,状况就有些不同了。

package main

import "fmt"

func main() {
    str := "Go 语言还是挺不错的"
    fmt.Println(len(str))   //26
}

  str 字符串蕴含 2 个英文字母,8 个中文汉字,输入显示字符串长度是 26。这就有 Go 语言字符串编码无关了,Go 语言字符串采取 utf- 8 编码,一个中文汉字占 3 个字节,所以算下来字符串长度就是 26 了。那的确想获取字符串的字符数目呢?可通过上面形式:

package main

import "fmt"

func main() {
    str := "Go 语言还是挺不错的"

    r := []rune(str)   //rune 其实就是 int32,4 字节示意一个字符;r 相当于字符切片
    fmt.Println(len(r))

    n := 0
    for _, _ = range str {    //range 遍历字符串,返回字符索引,与以后字符
        n ++
    }
    fmt.Println(n)
}

实现原理

  上面咱们将联合底层实现原理,一一解释下面的几种状况:字符串相加,字符串与 byte 切片转换,字符串与 rune 切片相互转换。

  字符串构造定义以及基本操作能够在文件 runtime/string.go 查看,字符串构造定义比切片相似,略微简略些,因为字符串只读,所以没有必要预调配空间,也就不须要 cap 字段:

type stringStruct struct {
    str unsafe.Pointer
    len int
}

  str 指向底层真正存储字符串的数组,只是咱们不能获取到该数组援用,所以也就无奈间接批改字符串。而字符串在作为输出参数时,传递的也是该构造;len 函数获取字符串长度时,字符串变量地址 + 8 字节就是了,这些都和上一大节切片的基本原理十分相似。

  s 字符串相加,编译阶段会替换为函数调用 concatstrings,其实现也挺简略的,计算所有字符串长度之和,申请内存,拷贝原始多个字符串到新的内存,结构字符串构造体 stringStruct 返回。函数 concatstrings 外围逻辑如下:

func concatstrings(a []string) string {
    l := 0
    // 计算所有字符串长度之和
    for _, x := range a {n := len(x)
        l += n
    }

    var s string
    var b []byte
    // 申请内存
    p := mallocgc(uintptr(l), nil, false)
    // 结构字符串 stringStruct 构造
    (*stringStruct)(unsafe.Pointer(s)).str = p
    (*stringStruct)(unsafe.Pointer(s)).len = l

    // 借切片拷贝
    *(*slice)(unsafe.Pointer(&b)) = slice{p, l, l}
    for _, x := range a {copy(b, x)
        b = b[len(x):]
    }

    return s
}

  看到了吧,字符串相加,是申请了新的内存,并执行了数据拷贝,原始字符串没有产生任何扭转,往往扭转的只是字符串变量指向的内存地址。

  字符串转化为 byte 切片,批改切片,为什么字符串却没有扭转,要答复这个问题,只能看字符串转化切片的实现函数了。通过 []byte(“”) 模式类型强转,编译阶段会替换为函数调用 stringtoslicebyte,而该函数其实也是申请新的内存,拷贝数据,结构切片构造返回。函数 stringtoslicebyte 外围逻辑如下:

func stringtoslicebyte(s string) []byte {var b []byte
    // 申请内存
    cap := roundupsize(uintptr(len(s)))
    p := mallocgc(cap, nil, false)

    // 结构切片构造 & 拷贝数据
    *(*slice)(unsafe.Pointer(&b)) = slice{p, len(s), int(cap)}
    copy(b, s)

    return b
}

  字符串转化为 rune 切片的逻辑与 stringtoslicebyte 十分相似,只是 rune 类型占 4 个字节罢了。这里就不再赘述了。

罕用库函数 & stringBuilder

  包 strings 定义了一些罕用的字符串库函数,如下:

// 字符串比拟
func Compare(a, b string) int
// 字符串是否以 xxx 开始
func HasPrefix(s, prefix string) bool
// 字符串是否以 xxx 完结
func HasSuffix(s, suffix string) bool
// 字符串是否蕴含指定子串
func Contains(s, substr string) bool
// 返回子串在字符串是的地位,- 1 字符串不蕴含子串,还有更高级的字符串查找 stringFinder
func Index(s, substr string) int
// 字符串数组转换为字符串,按 sep 分隔
func Join(elems []string, sep string) string
// 字符串分隔为字符串数组
func Split(s, sep string) []string
// 字符串替换,还有更高级的字符串替换 Replacer
func Replace(s, old, new string, n int) string
// 字符串大小写转换
func ToLower(s string) string
func ToUpper(s string) string

……

  这些库函数非常简单,我就不一一介绍了,这里次要提一下字符串构建 stringBuilder。下面咱们说过 Go 语言字符串是只读的,不能批改的,字符串相加也是通过申请内存与数据拷贝形式实现,那么如果存在大量的字符串相加呢?每次都申请内存拷贝数据效率会十分差,这也是 stringBuilder 存在的起因。stringBuilder 底层保护了一个[]byte,追加字符串只是追加到该切片,最终一次性转换该切片为字符串,防止了两头 N 屡次的内存申请与数据拷贝。

  咱们写一个小事例,测试验证大量字符串相加状况下,stringBuilder 带来的性能晋升:

package main

import (
    "fmt"
    "strings"
    "time"
)

func main() {
    count := 100000

    start := time.Now()
    s := ""
    for i := 0; i < count; i ++ {s += "abc"}
    fmt.Println(time.Now().Sub(start).Microseconds())   //1466286 奥妙

    start1 := time.Now()
    b := strings.Builder{}
    for i := 0; i < count; i ++ {b.WriteString("abc")
    }
    fmt.Println(time.Now().Sub(start1).Microseconds()) //492 奥妙,效率晋升非常明显。}

字符串编码

  下面咱们介绍到,Go 语言一个汉字占 3 字节,所有字符串蕴含汉字时,len 返回字符串长度大于字符数。咱们都晓得计算机存储只辨认二进制,所以字符须要编码为二进制,那么 Go 语言字符串到底采取哪种编码方式呢?

  先简略介绍下几个罕用编码。最简略的编码就是 ASCII 码了,只需一个字节,能够示意一些根本的字符、数字与字母,如 ”? \ ! . 10 A b c”。那么中文怎么办?一个字节必定是无奈满足的。于是诞生了 unicode 编码,占两个字节,能够包容所有语言的大部分文字。在 unicode 编码方式下,所有字符都须要两个字节,不管汉字还是字母(高字节全 0,低字节就是 ASCII 码),显然对于字母有些节约空间了。所以又诞生了 utf- 8 编码,这时候字符能够占 1 - 4 字节(可变的),中文汉字在 utf- 8 编码方式占 3 个字节,英文字母占 1 个字节,Go 语言采纳的就是该编码方式。这下终于明确了如何计算蕴含汉字的字符串长度了。

  话不多说,再来个小事例测试一下:

package main

import "fmt"

func main() {
    str := "Go 语言还是挺不错的"
    r := []rune(str)
    for _, v := range r{fmt.Printf("%x", v)
    }
}

//47 6f 8bed 8a00 8fd8 662f 633a 4e0d 9519 7684

  如同有些许不对劲,这些汉字貌似只占了 2 字节,这些是 utf- 8 编码吗?其实下面输入的都是 unicode 编码,所有字符都占 2 字节。读者能够找一些工具测试下,将下面字符串转换为 unicode,比照看后果是否统一。

  必须要阐明的是,rune 其实就是 int32,该类型原本就占 4 字节。Go 语言字符串在存储时,的确是采纳 utf- 8 编码,然而当转化为[]rune 操作时,又将所有字符转化为 unicode 编码。字符串与[]rune 转化函数为 stringtoslicerune/slicerunetostring。unicode 编码与 utf- 8 编码转化函数定义在文件 runtime/utf8.go,别离为 decoderune/encoderune。

  这里就不具体介绍这几个函数的具体实现了。不过须要留神的是,在应用 range 遍历字符串时,返回的是字符,也存在 utf- 8 到 unicode 编码转换。range 的实现逻辑在源码中找不到,是编译阶段主动生成的,如下:

// 参考:cmd/compile/internal/walk/range.go:walkRange

// Transform string range statements like "for v1, v2 = range a" into
//
// ha := a
// for hv1 := 0; hv1 < len(ha); {
//   hv1t := hv1
//   hv2 := rune(ha[hv1])
//   if hv2 < utf8.RuneSelf {
//      hv1++
//   } else {//      hv2, hv1 = decoderune(ha, hv1)
//   }
//   v1, v2 = hv1t, hv2
//   // original body
// }

总结

  字符串的根本应用与实现原理就解说到这里了,要牢记字符串是不可读的,而字符串相加,字符串与[]byte/[]rune 相互转换都是通过申请内存以及数据拷贝形式实现的。另外要留神中文汉字编码占 3 个字节,所以蕴含中文汉字的字符串,其长度与字符数是不同的。

退出移动版