Golang---基本类型(string)
摘要:由于在实习过程中,做的项目都是基于 Golang 语言,所以在面试时,面试官也一定会理所当然的问 Golang, 所以在最近一段时间,主要学习这门语言的基础知识,以及常出的面试题。
简单介绍
字符串虽然在 Go 语言中是基本类型 string, 但是它实际上是由字符组成的数组,类似于 C 语言中的 char [] ,作为数组会占用一片连续的内存空间。Go 语言中的字符串其实只是一个只读的字节数组,不支持直接修改 string 类型变量的内存空间,比如下面代码就是不支持的:
package main import ( "fmt" ) func main() { s := "hello" s[0] = 'A' fmt.Println(s) } //.\main.go:9:7: cannot assign to s[0]
如果我们想修改字符串,我们可以将这段内存拷贝到堆或者栈上,将遍历的类型转换为 []byte 之后就可以进行,修改后通过类型转换就可以变回 string, 对原变量重新赋值即可。
package main import ( "fmt" ) func main() { s := "hello" sByte := []byte(s) sByte[0] = 'A' //重新赋值 s = string(sByte) fmt.Println(s) } //Aello
数据结构
字符串在 Go 语言中的接口其实非常简单,每一个字符串在运行时都会使用如下的 StringHeader 结构体表示,其实在”运行时“内部,有一个私有的结构 stringHeader, 它有着完全相同的结构,只是用于存储数据的 Data 字段使用了 unsafe.Pointer 类型:
// StringHeader is the runtime representation of a string. // It cannot be used safely or portably and its representation may // change in a later release. // Moreover, the Data field is not sufficient to guarantee the data // it references will not be garbage collected, so programs must keep // a separate, correctly typed pointer to the underlying data. type StringHeader struct { Data uintptr Len int } // stringHeader is a safe version of StringHeader used within this package. type stringHeader struct { Data unsafe.Pointer Len int }
声明方式
使用双引号
s := "hello world"
使用反引号
s := `hello world`
使用双引号可其它语言没有什么大的区别,如果字符串内部出现双引号,要使用 \ 进行转义;但使用反引号则不需要,方便进行更加复杂的数据类型,比如 Json:
s := `{"name": "sween", "age": 18}`
注:上面两种格式的解析函数分别为cmd/compile/internal/syntax.scanner.stdString
cmd/compile/internal/syntax.scanner.rawString
类型转换
在我们使用 Go 语言解析和序列化 Json 等数据格式时,经常需要将数据在 string 和 []byte 之间进行转换,类型转换的开销其实并没有想象中的那么小。
[]byte 到 string 的转换
runtime.slicebytetostring 这个函数中进行转换的处理,我们看下源码:
// slicebytetostring converts a byte slice to a string. // It is inserted by the compiler into generated code. // ptr is a pointer to the first element of the slice; // n is the length of the slice. // Buf is a fixed-size buffer for the result, // it is not nil if the result does not escape. func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str 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 n == 1 { p := unsafe.Pointer(&staticuint64s[*ptr]) if sys.BigEndian { p = add(p, 7) } stringStructOf(&str).str = p stringStructOf(&str).len = 1 return } var p unsafe.Pointer if buf != nil && n <= len(buf) { p = unsafe.Pointer(buf) } else { //step1: 分配内存空间 p = mallocgc(uintptr(n), nil, false) } stringStructOf(&str).str = p stringStructOf(&str).len = n //step2:执行内存拷贝操作 memmove(p, unsafe.Pointer(ptr), uintptr(n)) return }
string 到 []byte 的转换
runtime.stringtoslicebyte 这个函数中进行转换的处理,我们看下源码:
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { //step1: 如果缓冲区够用,直接用 *buf = tmpBuf{} b = buf[:len(s)] } else { //step2: 如果缓冲区不够用,重新分配一个 b = rawbyteslice(len(s)) } //step3: 执行内存拷贝操作 copy(b, s) return b } // rawbyteslice allocates a new byte slice. The byte slice is not zeroed. func rawbyteslice(size int) (b []byte) { cap := roundupsize(uintptr(size)) p := mallocgc(cap, nil, false) if cap != uintptr(size) { memclrNoHeapPointers(add(p, uintptr(size)), cap-uintptr(size)) } *(*slice)(unsafe.Pointer(&b)) = slice{p, size, int(cap)} return }
总结
字符串和 []byte 中的内容虽然一样,但是字符串的内容是只读的,我们不能通过下标或者其它形式改变其中的数据,而 []byte 中的内容是可读写的,无论哪种类型转换到另一种类型都需要对其中的内容进行拷贝,而内存拷贝的性能损耗会随着字符串和 []byte 长度的增长而增长。所以在做类型转换时候一定要注意性能的损耗。
参考资料:
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-string/