golang深度学习-基础篇
- 互联网
- 2025-08-26 08:51:02

基础数据结构及类型 字符型-string
string 是Go标准库 buildin 内置的一个基础数据类型。string是由8比特字节的集合,通常不一定是UTF-8编码的文本。string可以为空(长度为0),但不会是nil。
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.
在Go的实现中,string不包含内存空间,只有一个内存的指针,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。 因为string通常指向字符串字面量,而字符串字面量存储位置是只读段(静态存储区),而不是堆或栈上,所以才有了string不可修改的约定,但是可以通过切片间接修改内容。
string的本质字符串的终止有两种方式,一种是C语言中的隐式申明,以字符“\0”作为终止符。一种是Go语言中的显式声明。Go语言运行时字符串string的表示结构如下。
type StringHeader struct { // 运行时代表 Data unitptr // 指向底层的字符数组 Len int // 字符串长度 } type stringStruct struct { str unsafe.Pointer // 字符串的首地址 len int // 字符串的长度 }字符串在本质上是一串字符数组,每个字符在存储时都对应了一个或多个整数,这涉及字符集的编码方式。Go语言中所有的文件都采用UTF-8的编码方式,同时字符常量使用UTF-8的字符编码集。UFT-8是一种长度可变的编码方式,可包含世界上大部分的字符。 在Go语言中使用符文(rune)类型来表示和区分字符串中的“字符”,rune其实是int32的别称。当用range轮询字符串时,轮询的不再是单字节,而是具体的rune。
在标准库strings包中包含字符查找、分割、大小写转换、trim修剪等数十个函数。在标准库strconv包中,还包含很多字符串与其他类型进行转换的函数。
字符串拼接当拼接后的s字符串小于32字节时,会有一个临时的缓存供其使用。当拼接后的字符串大于32字节时,会请求在堆区分配内存。 大于32字节时的具体过程:一个拼接语句的字符串编译时都会被存放到一个切片中,拼接过程需要遍历两次切片,第一次遍历获取总的字符串长度,据此申请内存,第二次遍历会把字符串逐个拷贝过去。即便有非常多的字符串需要拼接,性能上也有比较好的保证,因为新字符串的内存空间是一次分配完成的,所以性能消耗主要在拷贝数据上。
字符串与字节数组的转换字节数组与字符串可以相互转换。b := []byte(a)、c := string(b)。 字节数组转换为字符串在运行时调用了slicebytetostring函数。字节数组与字符串的相互转换并不是简单的指针引用,而是涉及了复制。当字符串大于32字节时,还需要申请堆内存,因此在涉及一些密集的转换场景时,需要评估这种转换带来的性能损耗。
func slicebytetostring(buf *tmpBuf, ptr *byte, n int) (str string) { // ... 省略多余代码 if n == 1 { p := unsafe.Pointer(&staticuint64s[*ptr]) if goarch.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 { p = mallocgc(uintptr(n), nil, false) // 分配内存 } stringStructOf(&str).str = p stringStructOf(&str).len = n memmove(p, unsafe.Pointer(ptr), uintptr(n)) return }当字符串转换为字节数组时,在运行时需要调用stringtoslicebyte函数,其和slicebytetostring函数非常类似,需要新的足够大小的内存空间。当字符串小于32字节时,可以直接使用缓存buf。当字符串大于32字节时,rawbyteslice函数需要向堆区申请足够的内存空间。最后使用copy函数完成内存复制。
func stringtoslicebyte(buf *tmpBuf, s string) []byte { var b []byte if buf != nil && len(s) <= len(buf) { *buf = tmpBuf{ } b = buf[:len(s)] } else { b = rawbyteslice(len(s)) // 重新申请内存 } 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 }如果不希望发生内存拷贝,可以直接通过指针的方式完成转换。
// String2Bytes 高性能的转换 string 为 []byte func String2Bytes(s string) []byte { x := (*[2]uintptr)(unsafe.Pointer(&s)) h := [3]uintptr{ x[0], x[1], x[1]} return *(*[]byte)(unsafe.Pointer(&h)) } // Bytes2String 高性能的转换 []byte 为 string // 比直接使用string(b)的性能强悍4-5倍 func Bytes2String(b []byte) string { return *(*string)(unsafe.Pointer(&b)) }byte切片转换成string的场景很多,为了性能上的考虑,有时候只是临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存。如:
使用m[string(b)]来查找map(map是string为key,临时把切片b转成string); 字符串拼接,如”<” + “string(b)” + “>”; 字符串比较:string(b) == “foo” 数组数组是一片连续的内存区域,其不能进行扩容、在复制和传递时为值复制。数组形如[n]T,在编译时就需要确定其长度和类型。当数组的长度小于4时,在运行时数组会被放置在栈中,当数组的长度大于4时,数组会被放置到内存的静态只读区。
使用未命名常量索引访问数组时,数组的一些简单越界错误能够在编译期间被发现。但是如果使用变量去访问数组或者字符串,编译器无法发现对应的错误,因为变量的值随时可能变化。数组访问索引异常(非整数、越界、负数)时会发生panic。
切片-sliceSlice又称动态数组,依托数组实现的长度可变的序列,可以方便的进行扩容、传递等,实际使用中比数组更灵活。
数据结构一个切片在运行时由指针(date)、长度(len)和容量(cap)3部分构成。指针指向切片元素对应的底层数组元素的地址。长度对应切片中元素的数目,长度不能超过容量。容量一般是从切片的开始位置到底层数据的结尾位置的长度。
type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 }在Go语言中,切片的复制其实也是值复制,但这里的值复制指对于运行时SliceHeader结构的复制。 底层指针仍然指向相同的底层数据的数组地址,因此可以理解为数据进行了引用传递。切片的这一特性使得即便切片中有大量数据,在复制时的成本也比较小,这与数组有显著的不同。 但实际上,切片可能分配在堆内存上,而数组会被分配到栈上。如果数组容量在相对较小的情况下可能要比切片的分配效率要高。
slice 和数组的区别 slice 的底层数据是数组,slice 是对数组的封装,它描述一个数组的片段。两者都可以通过下标来访问单个元素。 数组是定长的,长度定义好之后,不能再更改。在 Go 中,数组是不常见的,因为其长度是类型的一部分,限制了它的表达能力,比如 [3]int 和 [4]int 就是不同的类型。 当 slice 作为函数参数时,就是一个普通的结构体,会发生复制。尽管函数参数传递只有值传递没有引用传递,但如果改变了 slice 底层数组的数据,会反应到实参 slice 的底层数据。这是因为底层数据在 slice 结构体里是一个指针,仅管 slice 结构体自身不会被改变,但是通过指向底层数据的指针,可以改变切片的底层数据。 slice 的创建 直接声明:var slice []int,声明一个nil slice,长度和容量都为0,和nil比较的结果为true。 new:slice := *new([]int),创建一个nil slice,长度和容量都为0,和nil比较的结果为true。 字面量:slice := []int{},声明一个empty slice,长度和容量都为0,和nil比较的结果为false(array指针有值)。 字面量:slice := []int{1,2,3,4,5},直接用初始化表达式创建切片。 make:slice := make([]int, 0),初始化一个empty slice,长度和容量都为0,和nil比较的结果为false(array指针有值)。 make:slice := make([]int, 5, 10),初始化一个slice,长度为5、容量为10。 从切片或数组“截取”:slice := array[1:5] 或 slice := sourceSlice[1:5]。nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。 基于已有 slice 创建新 slice 对象,被称为 reslice。新 slice 和老 slice 共用底层数组,新老 slice 对底层数组的更改都会影响到彼此。基于数组创建的新 slice 对象也是同样的效果:对数组或 slice 元素作的更改都会影响到彼此。 但如果因为执行 append 操作使得新 slice 底层数组扩容,移动到了新的位置,两者就不会相互影响了。
扩容机制使用 append 函数可以向切片中追加元素。append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。append函数返回值是一个新的slice,Go编译器不允许调用了 append 函数后不使用返回值。
// append 函数 func append(slice []Type, elems ...Type) []Type // 追加元素 slice = append(slice, elem1, elem2) slice = append(slice, anotherSlice...)使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引len-1所指向的元素已经是底层数组的最后一个元素,就会触发扩容机制。扩容扩大的容量都是针对原来的容量而言的,而不是针对原来数组的长度而言的。
扩容的具体思路是:当原 slice 容量小于 阈值(256,不同版本阈值不同,旧本版是1024) 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 阈值,新 slice 容量按照1.25倍的速度依次递增,直到最终容量大于等于新申请的容量。之后会根据内存分配策略进行内存对齐roundupsize函数,因此最终扩容的容量总是要 大于等于 老 slice 容量的 2倍 或者1.25倍。
func growslice(et *_type, old slice, cap int) slice { // ... 省略无关代码 // 发生了溢出 if cap < old.cap { panic(errorString("growslice: cap out of range")) } // 容量为0的切片要扩容,直接生成一个新的切片返回 if et.size == 0 { return slice{ unsafe.Pointer(&zerobase), old.len, cap} } // 扩容机制 newcap := old.cap doublecap := newcap + newcap // 2倍 if cap > doublecap { newcap = cap } else { const threshold = 256 // 不同版本下,阈值不同,此处Go1.18是256,旧版本是1024。 if old.cap < threshold { newcap = doublecap } else { // 防止溢出和死循环 for 0 < newcap && newcap < cap { // 按0.25倍的逐渐向上加,知道大于目标容量为止 newcap += (newcap + 3*threshold) / 4 } // 再次防止溢出 if newcap <= 0 { newcap = cap } } } // 计算新的切片的容量,长度。会对 newcap 作一个内存对齐操作( roundupsize 函数),计算方式和内存分配策略相关。 // 因为需要保证申请内存后不浪费空间,因此,进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。 // ... 省略大部分代码,仅保留核心代码 capmem = roundupsize(uintptr(newcap)) newcap = int(capmem<golang深度学习-基础篇由讯客互联互联网栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“golang深度学习-基础篇”