Go语言模型:string的底层数据结构与高效操作详解

Golang的string类型底层数据结构简单,本质也是一个结构体实例,且是const不可变。

string的底层数据结构

通过下面一个例子来看:

package main
import (
	"fmt"
	"unsafe"
)
// from: string.go 在GoLand IDE中双击shift快速找到
type stringStruct struct {
	array unsafe.Pointer // 指向一个 [len]byte 的数组
	length int    // 长度
}
func main() {
	test := "hello"
	p := (*str)(unsafe.Pointer(&test))
	fmt.Println(&p, p) // 0xc420070018 &{0xa3f71 5}
	c := make([]byte, p.length)
	for i := 0; i < p.length; i++ {
		tmp := uintptr(unsafe.Pointer(p.array))   // 指针类型转换通过unsafe包
		c[i] = *(*byte)(unsafe.Pointer(tmp + uintptr(i))) // 指针运算只能通过uintptr
	}
	fmt.Println(c)   // [104 101 108 108 111]
	fmt.Println(string(c)) // [byte] --> string, "hello"
	test2 := test + " world" // 字符串是不可变类型,会生成一个新的string实例
	p2 := (*str)(unsafe.Pointer(&test2))
	fmt.Println(&p2, p2) // 0xc420028030 &{0xc42000a2e5 11}
	fmt.Println(test2) // hello, world
}

string的拼接与修改

+操作

string类型是一个不可变类型,那么任何对string的修改都会新生成一个string的实例,如果是考虑效率的场景就要好好考虑一下如何修改了。先说一下最长用的+操作,同样上面的例子,看一下+操作拼接字符串的反汇编:

25		test2 := test + " world"
 0x00000000004824d7 <+1127>:	lea 0x105a2(%rip),%rax  # 0x492a80
 0x00000000004824de <+1134>:	mov %rax,(%rsp)
 0x00000000004824e2 <+1138>:	callq 0x40dda0 <runtime.newobject> # 调用newobject函数
 0x00000000004824e7 <+1143>:	mov 0x8(%rsp),%rax
 0x00000000004824ec <+1148>:	mov %rax,0xa0(%rsp)
 0x00000000004824f4 <+1156>:	mov 0xa8(%rsp),%rax
 0x00000000004824fc <+1164>:	mov 0x8(%rax),%rcx
 0x0000000000482500 <+1168>:	mov (%rax),%rax
 0x0000000000482503 <+1171>:	mov %rax,0x8(%rsp)
 0x0000000000482508 <+1176>:	mov %rcx,0x10(%rsp)
 0x000000000048250d <+1181>:	movq $0x0,(%rsp)
 0x0000000000482515 <+1189>:	lea 0x30060(%rip),%rax  # 0x4b257c
 0x000000000048251c <+1196>:	mov %rax,0x18(%rsp)
 0x0000000000482521 <+1201>:	movq $0x6,0x20(%rsp)
 0x000000000048252a <+1210>:	callq 0x43cc00 <runtime.concatstring2> # 调用concatstring2函数

因为当前go[2018.11 version: go1.11]的不是遵循默认的x86 calling convention用寄存器传参,而是通过stack进行传参,所以go的反汇编不像c的那么容易理解,不过大概看懂+背后的操作还是没问题的,看一下runtime源码的拼接函数:

func concatstring2(buf *tmpBuf, a [2]string) string {
 return concatstrings(buf, a[:])
}
// concatstrings implements a Go string concatenation x+y+z+...
// The operands are passed in the slice a.
// If buf != nil, the compiler has determined that the result does not
// escape the calling function, so the string data can be stored in buf
// if small enough.
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]
 }
 s, b := rawstringtmp(buf, l)
 for _, x := range a {
  copy(b, x) // 最关键的拷贝操作
  b = b[len(x):]
 }
 return s
}

分析runtime的concatstrings实现,可以看出+最后新申请buf,拷贝原来的string到buf,最后返回新实例。那么每次的+操作,都会涉及新申请buf,然后是对应的copy。如果反复使用+,就不可避免有大量的申请内存操作,对于大量的拼接,性能就会受到影响了。

bytes.Buffer

通过看源码,bytes.Buffer 增长buffer时是按照2倍来增长内存,可以有效避免频繁的申请内存,通过一个例子来看:

func main() {
 var buf bytes.Buffer
 for i := 0; i < 10; i++ {
  buf.WriteString("hi ")
 }
 fmt.Println(buf.String())
}

对应的byte包库函数源码

// @file: buffer.go
func (b *Buffer) WriteString(s string) (n int, err error) {
 b.lastRead = opInvalid
 m, ok := b.tryGrowByReslice(len(s))
 if !ok {
  m = b.grow(len(s)) // 高效的增长策略 -> let capacity get twice as large
 }
 return copy(b.buf[m:], s), nil
}
// @file: buffer.go
// let capacity get twice as large !!!
func (b *Buffer) grow(n int) int {
 m := b.Len()
 // If buffer is empty, reset to recover space.
 if m == 0 && b.off != 0 {
  b.Reset()
 }
 // Try to grow by means of a reslice.
 if i, ok := b.tryGrowByReslice(n); ok {
  return i
 }
 // Check if we can make use of bootstrap array.
 if b.buf == nil && n <= len(b.bootstrap) {
  b.buf = b.bootstrap[:n]
  return 0
 }
 c := cap(b.buf)
 if n <= c/2-m {
  // We can slide things down instead of allocating a new
  // slice. We only need m+n <= c to slide, but
  // we instead let capacity get twice as large so we
  // don't spend all our time copying.
  copy(b.buf, b.buf[b.off:])
 } else if c > maxInt-c-n {
  panic(ErrTooLarge)
 } else {
  // Not enough space anywhere, we need to allocate.
  buf := makeSlice(2*c + n)
  copy(buf, b.buf[b.off:])
  b.buf = buf
 }
 // Restore b.off and len(b.buf).
 b.off = 0
 b.buf = b.buf[:m+n]
 return m
}

string.join

这个函数可以一次申请最终string的大小,但是使用得预先准备好所有string,这种场景也是高效的,一个例子:

func main() {
 var strs []string
 for i := 0; i < 10; i++ {
 strs = append(strs, "hi")
 }
 fmt.Println(strings.Join(strs, " "))
}

对应库的源码:

// Join concatenates the elements of a to create a single string. The separator string
// sep is placed between elements in the resulting string.
func Join(a []string, sep string) string {
 switch len(a) {
 case 0:
  return ""
 case 1:
  return a[0]
 case 2:
  // Special case for common small values.
  // Remove if golang.org/issue/6714 is fixed
  return a[0] + sep + a[1]
 case 3:
  // Special case for common small values.
  // Remove if golang.org/issue/6714 is fixed
  return a[0] + sep + a[1] + sep + a[2]
 }

 // 计算好最终的string的大小
 n := len(sep) * (len(a) - 1) //
 for i := 0; i < len(a); i++ {
  n += len(a[i])
 }
 b := make([]byte, n)
 bp := copy(b, a[0])
 for _, s := range a[1:] {
  bp += copy(b[bp:], sep)
  bp += copy(b[bp:], s)
 }
 return string(b)
}

strings.Builder (go1.10+)

看到这个名字,就想到了Java的库,哈哈,这个Builder用起来是最方便的,不过是在1.10后引入的。其高效也是体现在2倍速的内存增长, WriteString函数利用了slice类型对应append函数的2倍速增长。

一个例子:

func main() {
 var s strings.Builder
 for i := 0; i < 10; i++ {
  s.WriteString("hi ")
 }
 fmt.Println(s.String())
}

对应库的源码

@file: builder.go
// WriteString appends the contents of s to b's buffer.
// It returns the length of s and a nil error.
func (b *Builder) WriteString(s string) (int, error) {
 b.copyCheck()
 b.buf = append(b.buf, s...)
 return len(s), nil
}

总结

Golang的字符串处理还是挺方便的,有垃圾回收和一些内置的语言级写法支持,让复杂字符串操作没有那么繁琐了,比起C/C++高效了不少。

补充:go string的内部实现

go string 内部实现

这个string的探索

来来个例子

func boo(a int, b int)(int, string){
 return a + b, "abcd"
}
81079 000000000044dfa0 <main.boo>:
81080 44dfa0:>------48 c7 44 24 18 00 00 >--movq $0x0,0x18(%rsp)
81081 44dfa7:>------00 00-
81082 44dfa9:>------0f 57 c0    >--xorps %xmm0,%xmm0
81083 44dfac:>------0f 11 44 24 20  >--movups %xmm0,0x20(%rsp)
81084 44dfb1:>------48 8b 44 24 08  >--mov 0x8(%rsp),%rax
81085 44dfb6:>------48 03 44 24 10  >--add 0x10(%rsp),%rax
81086 44dfbb:>------48 89 44 24 18  >--mov %rax,0x18(%rsp)
81087 44dfc0:>------48 8d 05 d4 eb 01 00 >--lea 0x1ebd4(%rip),%rax  # 46cb9b <go.string.*+0xbb>
81088 44dfc7:>------48 89 44 24 20  >--mov %rax,0x20(%rsp)
81089 44dfcc:>------48 c7 44 24 28 04 00 >--movq $0x4,0x28(%rsp)
81090 44dfd3:>------00 00-
81091 44dfd5:>------c3     >--retq---

其中

81087 44dfc0:>------48 8d 05 d4 eb 01 00 >--lea 0x1ebd4(%rip),%rax  # 46cb9b <go.string.*+0xbb>
81088 44dfc7:>------48 89 44 24 20  >--mov %rax,0x20(%rsp)
81089 44dfcc:>------48 c7 44 24 28 04 00 >--movq $0x4,0x28(%rsp)
81090 44dfd3:>------00 00-
81091 44dfd5:>------c3     >--retq---
lea 0x1ebd4(%rip),%rax得到char*, mov %rax,0x20(%rsp)复制给返回值, movq $0x4,0x28(%rsp)把长度也填进去,

其实可以看到string就是c里面的char* 和len的组合

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。如有错误或未考虑完全的地方,望不吝赐教。

(0)

相关推荐

  • strings命令分析浅谈Go和C++编译时的一点小区别

    最近查一个bug, 用strings命令分析, 竟然出乎意料地没有结果, 非常纳闷. 最后根据这个线索查出了bug的根本原因. 1.  在C++中, 即使函数在代码层面没有被调用, 也会最终编译到二进制中, 用strings可以分析.  #include <iostream> using namespace std; void fun() { printf("hello world\n"); // strings分析有结果 } int main() { return 0;

  • golang语言如何将interface转为int, string,slice,struct等类型

    在golang中,interface{}允许接纳任意值,int,string,struct,slice等,因此我可以很简单的将值传递到interface{},例如: package main import ( "fmt" ) type User struct{ Name string } func main() { any := User{ Name: "fidding", } test(any) any2 := "fidding" test(a

  • Go中strings的常用方法详解

    string操作在编程中具有极高的频率,那么string中有哪些有用的方法呢? 使用strings直接操作 Compare func Compare(a, b string) int 按照字典序比较两个字符串,通常情况下直接使用=,>,<会更快一些. Contains,ContainsAny 和 ContainsRune func Contains(s, substr string) bool func ContainsAny(s, chars string) bool func Contai

  • go 迭代string数组操作 go for string[]

    go 迭代string数组,直接拷贝去用即可 package main import ( "fmt" ) func main() { subsCodes := []string{"aaaa", "vvvvv", "dddd", "eeeee", "gfgggg"} for _, s := range subsCodes { fmt.Println(s) } } 补充:golang字符串s

  • Go语言string,int,int64 ,float之间类型转换方法

    (1)int转string s := strconv.Itoa(i) 等价于s := strconv.FormatInt(int64(i), 10) (2)int64转string i := int64(123) s := strconv.FormatInt(i, 10) 第二个参数为基数,可选2~36 注:对于无符号整形,可以使用FormatUint(i uint64, base int) (3)string转int i, err := strconv.Atoi(s) (4)string转in

  • go语言中strings包的用法汇总

    strings 包中的函数和方法 // strings.go ------------------------------------------------------------ // Count 计算字符串 sep 在 s 中的非重叠个数 // 如果 sep 为空字符串,则返回 s 中的字符(非字节)个数 + 1 // 使用 Rabin-Karp 算法实现 func Count(s, sep string) int func main() { s := "Hello,世界!!!!!&quo

  • Go语言中strings和strconv包示例代码详解

    前缀和后缀 HasPrefix判断字符串s是否以prefix开头: strings.HaxPrefix(s string, prefix string) bool 示例: package main import ( "fmt" "strings" ) func main() { pre := "Thi" str1 := "This is a Go program!" fmt.Println(strings.HasPrefix(

  • Go语言模型:string的底层数据结构与高效操作详解

    Golang的string类型底层数据结构简单,本质也是一个结构体实例,且是const不可变. string的底层数据结构 通过下面一个例子来看: package main import ( "fmt" "unsafe" ) // from: string.go 在GoLand IDE中双击shift快速找到 type stringStruct struct { array unsafe.Pointer // 指向一个 [len]byte 的数组 length in

  • Java数据结构顺序表用法详解

    目录 1.什么是顺序表 2.顺序表的基本功能和结构 3.顺序表基本功能的实现和解析 1.判断线性表是否为空 2.获取指定位置的元素 3.向线性表表添加元素 4.在位置i处插入元素 5.删除指定位置的元素,并返回该元素 6.查找t第一次出现的位置 7.手动扩容方法 1.什么是顺序表 在程序中,经常需要将一组(通常是同为某个类型的)数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等.一组数据中包含的元素个数可能发生变化(可以增加或删除元素). 对于这种需求,最简单的解决方

  • Java数据结构之KMP算法详解以及代码实现

    目录 暴力匹配算法(Brute-Force,BF) 概念和原理 next数组 KMP匹配 KMP全匹配 总结 我们此前学了前缀树Trie的实现原理以及Java代码的实现.Trie树很好,但是它只能基于前缀匹配实现功能.但是如果我们的需求是:一个已知字符串中查找子串,并且子串并不一定符合前缀匹配,那么此时Trie树就无能为力了. 实际上这种字符串匹配的需求,在开发中非常常见,例如判断一个字符串是否包括某些子串,然后进行分别的处理. 暴力匹配算法(Brute-Force,BF) 这是最常见的算法字符

  • Java数据结构之对象比较详解

    目录 1. PriorityQueue中插入对象 2. 元素的比较 2.1 基本类型的比较 2.2 对象比较的问题 3. 对象的比较 3.1 覆写基类的equals 3.2 基于Comparble接口类的比较 3.3 基于比较器比较 3.4 三种方式的对比 4.集合框架中PriorityQueue的比较方式 本篇博客主要内容: Java中对象的比较 集合框架中PriorityQueue的比较方式 模拟实现PriorityQueue 1. PriorityQueue中插入对象 优先级队列在插入元素

  • c++ 数据结构map的使用详解

    map的常用用法 map 表示映射,可以将任何基本类型(包括 STL 容器)映射到任何基本类型(包括 STL 容器),例如可以建立如 int 到 double,string 到 int 的映射等. map 提供一对一的 hash,该功能类似 Python 的字典: 第一个称为键( key ),每个关键字只能在 map 中出现一次: 第二个称为该键的值( value ): 1. 头文件 <bits/stdc++.h> 头文件已经包括了该头文件. 2. 定义 定义 map 如下,参数的第一个为 k

  • C语言数据结构之单向链表详解分析

    链表的概念:链表是一种动态存储分布的数据结构,由若干个同一结构类型的结点依次串连而成. 链表分为单向链表和双向链表. 链表变量一般用指针head表示,用来存放链表首结点的地址. 每个结点由数据部分和下一个结点的地址部分组成,即每个结点都指向下一个结点.最后一个结点称为表尾,其下一个结点的地址部分的值为NULL(表示为空地址). 特别注意:链表中的各个结点在内存中是可以不连续存放的,具体存放位置由系统分配. 例如:int *ptr ; 因此不可以用ptr++的方式来寻找下一个结点. 使用链表的优点

  • Java数据结构之散列表详解

    目录 介绍 1 散列表概述 1.1 散列表概述 1.2 散列冲突(hash collision) 2 散列函数的选择 2.1 散列函数的要求 2.2 散列函数构造方法 3 散列冲突的解决 3.1 分离链接法 3.2 开放定址法 3.3 再散列法 4 散列表的简单实现 4.1 测试 介绍 本文详细介绍了散列表的概念.散列函数的选择.散列冲突的解决办法,并且最后提供了一种散列表的Java代码实现. 数组的特点是寻址容易,插入和删除困难:而链表的特点是寻址困难,插入和删除容易.而对于tree结构,它们

  • C语言数据结构哈希表详解

    /* * 程序名:hash.c,此程序演示哈希表的实现,数据元素单链表带头结点. * */ #include <stdio.h> #include <stdlib.h> #include <string.h> // 哈希表中数据元素的结构体. typedef struct Element { unsigned int key; // 关键字. int value; // 数据元素其它数据项,可以是任意数据类型. // char value[1001]; // 数据元素其

  • Java数据结构之线段树详解

    目录 介绍 代码实现 线段树构建 区间查询 更新 总结 介绍 线段树(又名区间树)也是一种二叉树,每个节点的值等于左右孩子节点值的和,线段树示例图如下 以求和为例,根节点表示区间0-5的和,左孩子表示区间0-2的和,右孩子表示区间3-5的和,依次类推. 代码实现 /** * 使用数组实现线段树 */ public class SegmentTree<E> { private Node[] data; private int size; private Merger<E> merge

  • SpringBoot2底层注解@Configuration配置类详解

    目录 SpringBoot2底层注解@Configuration配置类 一.配置类 二.配置类本身也是组件 三.proxyBeanMethods 属性 有组件依赖的场景 SpringBoot2底层注解@Configuration配置类 一.配置类 @Configuration这个注解作用就是告诉 springboot 这是一个配置类. 这个配置已经不陌生了,在之前 spring 相关的使用全注解方式时,就使用到了配置类. 在配置类里,可以使用@Bean标记在方法上,给容器注册组件,默认也是单实例

随机推荐