Go进阶之string

9 阅读7分钟

1.用法:

1.1声明:

声明一个空字符串变量在赋值.

func main() { var s1 string s1 = "hello world" }

注:空字符串只是长度为0.但不是nil.

简短变量声明:

func main() { s2 := "hello world" }

1.2双引号和反单引号区别:

字符串既可以使用双引号赋值.也可以使用反单引号赋值.它们的区别在于对特殊字符

串的处理.

示例:

    s := "hi, \nthis is \ngo"
    s1 := `hi,
         this is 
          go`
    fmt.Println(s)
    fmt.Println(s1)
}

使用双引号表示需要对特殊字符进行转义.单反引号不需要特殊字符转义.单反引号也

更直观.

1.3字符串拼接:

字符串可以使用加号进行拼接.

func main() {
    s := "a" + "b"
}

注:字符串拼接时会触发内存分配及内存拷贝.单行语句拼接多个字符串只分配一次内

存.

1.4类型转换:

[]byte转string.如下所示:

func main() {
    b := []byte{'h', 'e', 'l', 'l', 'o'}
    s := string(b)
    fmt.Println(s)
}

string转byte.如下所示:

func main() {
    s := "hello"
    bytes := []byte(s)
    fmt.Println(bytes)
}

注:无论是字符串转换成[]byte.还是[]byte转换成string.都会发生一次拷贝.会有一

定的开销.

2.特点:

2.1.UTF编码:

string使用8比特字节的集合来存储字符.而且存储的是字符的UTF-8编码.例如每个

汉字字符的UTF-8字符将占用多个字节.

使用for-range遍历字符串时.每次迭代返回返回字符UTF-8编码首个字节下标及字

符值.这意味这下标可能不连续,

示例:

func main() {
    s := "我是山西人"
    for index, value := range s {
       fmt.Printf("index: %d,value: %c\n", index, value)
    }
}

2.2值不可修改:

字符串可以为空.但值不会为nil.并且字符串不可以修改.字符串变量可以接受新的字

符串赋值.但不能通过下标方式修改字符串的值.

示例:

func main() {
    s := "hello"
    //非法
    &s[0] = byte(104)
    fmt.Println(s)
    //合法
    s = "Hello"
    fmt.Println(s)
}

注:字符串元素不支持取地址操作.所以无法修改字符串的值.所以上面会提示编译错

误.

3.标准库函数:

标准库strings包下提供了大量的字符串操作函数.如下:

strings.Contains   检查字符串s是否包含子串substr.

strings.Splict   将字符串s根据分割符sep拆分并生成子串的切片.

strings.Join   将字符串切片elems中的元素使用分隔符sep拼接成单个字符串.

strings.HasPrefix   检查字符串是否包含前缀prefix.

strings.HasSuffix   检查字符串s是否包含后缀suffix.

strings.ToUpper   将字符s所有的字符转换成大写.

strings.ToLower   将字符串s的所有字符转换成小写.

strings.Trim   将字符串s首部和尾部清除所有包含在cutset中的字符.

strings.TrimLeft   将字符串s首部清除所有包含在cutset中的字符.

strings.TrimRight   将字符串s尾部清除所有包含在cutset中的字符.

strings.TrimSpace   将字符串中首部尾部的空白字符清除掉.

strings.TrimPrefix   清除字符串s中的前缀prefix.

strings.TrimSuffix   清除字符串中的后缀suffix.

strings.Replace   将字符串s中的前n个子串old替换成子串new.

strings.ReplaceAll   将字符串中的所有子串old全部替换成子串new.

strings.EqualFold   忽略大小写.比较两个子串是否相等.

4.实现原理:

Go标准库builtin中定义了string类型:

// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string

1).string是比特字节的集合.通常是UTF-8的文本.

2).string可以为空(长度为0).但不会为nil.

3).string对象不可以修改.

4.1数据结构:

源码位于src/runtime/string.go:stringStruct中定义了string的数据结构.

type stringStruct struct {
    str unsafe.Pointer
    len int
}

1).str:字符串的首地址.

2).len:字符串的长度.string的数据结构与切片有点类似.唯一区别是切片还有一个字

段表示容量. string和

byte切片会经常发生切换.构建字符串的时候.会先使用gostringnocopy()函数生成

字符串.源码如下:

func gostringnocopy(str *byte) string {
    //先构建stringStruct.
    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
    //在转换成string.
    s := *(*string)(unsafe.Pointer(&ss))
    return s
}

string在runtime包中是stringStruct类型.对外呈现string类型.

4.2字符串表示:

字符串使用Unicode编码存储字符.对于英文字符.每个字符的Unicode编码只用一

字节即可表示.此时字符串长度等于字符数.对于非ASCII字符.需要多个字节表示.

此时字符串长度会大于实际字符数.字符串长度实际上是字节数.

4.3字符串拼接:

字符串的拼接内存空间是一次分配完成的.所以性能消耗主要在拷贝数据上.

在runtime包中.使用concatstrings()函数拼接字符串.在一个拼接语句中.所有待拼

接字符串都被编译器组织到一个切片中传入函数.拼接过程需要遍历两次切片.第一次

遍历会获取字符串长度.据此申请内存.第二次遍历会把字符串逐个拷贝过去.源码如

下:

func concatstrings(buf *tmpBuf, a []string) string {
    idx := 0
    l := 0
    count := 0
    //遍历计算每个字符的字节长度.
    for i, x := range a {
       n := len(x)
       if n == 0 {
          continue
       }
       if l+n < l {
          throw("string concatenation too long")
       }
       l += n
       count++
       idx = i
    }
    if count == 0 {
       return ""
    }

    // If there is just one string and either it is not on the stack
    // or our result does not escape the calling frame (buf != nil),
    // then we can return that string directly.
    if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
       return a[idx]
    }
    //分配内存.返回一个string和切片.二者共享内存空间.
    s, b := rawstringtmp(buf, l)
    for _, x := range a {
       n := copy(b, x)
       b = b[n:]
    }
    return s
}

因为string是无法直接修改的.这里使用rawstring()方法初始化一个指定大小的string.同时返回一个切片.二者共享内存空间.后面向切片中拷贝数据.也就间接修改了string.源码如下:

func rawstring(size int) (s string, b []byte) {
    p := mallocgc(uintptr(size), nil, false)
    return unsafe.String((*byte)(p), size), unsafe.Slice((*byte)(p), size)
}

可以看出string和切片密切相关.

4.4类型转换:

4.4.1[]byte转string:

byte切片可以很方便的转换成string.

func GetStringBySlice(s []byte) string {
    return string(s)
}

注:这种转换需要一次内存拷贝.

4.4.2转换过程:

1).根据切片长度申请内存空间.

2).构建string(string.str=p;string.len=len;)

3).拷贝数据(切片中的数据拷贝到新申请的内存空间).

在runtime包中使用slicebytetostring()函数将[]byte转换成string.源码如下:

func slicebytetostring(buf *tmpBuf, ptr *byte, n int) string {
    if n == 0 {
       // Turns out to be a relatively common case.
       // Consider that you want to parse out data between parens in "foo()bar",
       // you find the indices and convert the subslice to string.
       return ""
    }
    if raceenabled {
       racereadrangepc(unsafe.Pointer(ptr),
          uintptr(n),
          sys.GetCallerPC(),
          abi.FuncPCABIInternal(slicebytetostring))
    }
    if msanenabled {
       msanread(unsafe.Pointer(ptr), uintptr(n))
    }
    if asanenabled {
       asanread(unsafe.Pointer(ptr), uintptr(n))
    }
    if n == 1 {
       p := unsafe.Pointer(&staticuint64s[*ptr])
       if goarch.BigEndian {
          p = add(p, 7)
       }
       return unsafe.String((*byte)(p), 1)
    }

    var p unsafe.Pointer
    //如果预留buf够用.则用预留buf.
    if buf != nil && n <= len(buf) {
       p = unsafe.Pointer(buf)
    } else {
    //否则重新申请内存.
       p = mallocgc(uintptr(n), nil, false)
    }
    memmove(p, unsafe.Pointer(ptr), uintptr(n))
    //转换成字符串.
    return unsafe.String((*byte)(p), n)
}

4.4.3string转byte:

string也可以方便的转成byte切片.

func GetSliceByString(str string) []byte {
    return []byte(str)
}

string转换成byte切片也需要一次内存拷贝.过程如下:

1).申请切片内存空间.

2).将string拷贝到切片.

在runtime包中.使用stringtoslicebyte()函数将string转换成[]byte.源码如下:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte
    if buf != nil && len(s) <= len(buf) {
       *buf = tmpBuf{}
       //预留的buf中切出新的切片.
       b = buf[:len(s)]
    } else {
    //生成新的切片.
       b = rawbyteslice(len(s))
    }
    //进行拷贝.
    copy(b, s)
    return b
}

stringtoslicebyte()函数也使用了预留的buf.只有buf长度不够的时候才会申请内

存.rawbyteslice()函数用于申请新的未初始化的切片.由于字符串内容将完整覆盖切

片的存储空间.所以切片可以不初始化从而提升分配效率.

4.4.4编译优化:

byte转换成string的场景很多.出于性能考虑.在只是临时需要字符串的场景下.byte

切片转换成string时并不会拷贝内存.直接返回一个string.

识别场景:

使用m[string(b)]查找map(map的key类型为string时.临时把切片b转成string).

字符串拼接.如"<"+string(b)+">"

字符串比较.string(b)=="foo".

辗转反侧.铁马冰河.


如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路