GoLang strings.Builder底层实现方法详解

目录
  • 1.strings.Builder结构体
    • 1.1strings.Builder结构体
    • 1.2Write方法
    • 1.3WriteByte方法
    • 1.4WriteRune方法
    • 1.5.WriteString方法
    • 1.6String方法
    • 1.7Len方法
    • 1.8Cap方法
    • 1.9Reset方法
    • 1.10Grow方法
    • 1.11grow方法
    • 1.12copyCheck方法
  • 2.strings.Builder介绍
  • 3.存储原理
  • 4.拷贝问题
  • 5.不能与nil作比较
  • 6.Grow深入
  • 7.不支持并行读写

1.strings.Builder结构体

1.1strings.Builder结构体

// A Builder is used to efficiently build a string using Write methods.
// It minimizes memory copying. The zero value is ready to use.
// Do not copy a non-zero Builder.
type Builder struct {
	addr *Builder // of receiver, to detect copies by value
	buf  []byte
}

1.2Write方法

// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
	b.copyCheck()
	b.buf = append(b.buf, p...)
	return len(p), nil
}

1.3WriteByte方法

// WriteByte appends the byte c to b's buffer.
// The returned error is always nil.
func (b *Builder) WriteByte(c byte) error {
	b.copyCheck()
	b.buf = append(b.buf, c)
	return nil
}

1.4WriteRune方法

// WriteRune appends the UTF-8 encoding of Unicode code point r to b's buffer.
// It returns the length of r and a nil error.
func (b *Builder) WriteRune(r rune) (int, error) {
	b.copyCheck()
	// Compare as uint32 to correctly handle negative runes.
	if uint32(r) < utf8.RuneSelf {
		b.buf = append(b.buf, byte(r))
		return 1, nil
	}
	l := len(b.buf)
	if cap(b.buf)-l < utf8.UTFMax {
		b.grow(utf8.UTFMax)
	}
	n := utf8.EncodeRune(b.buf[l:l+utf8.UTFMax], r)
	b.buf = b.buf[:l+n]
	return n, nil
}

1.5.WriteString方法

// 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
}

1.6String方法

和 bytes.Buffer一样,strings.Builder 也支持使用 String() 来获取最终的字符串结果。为了节省内存分配,它通过使用指针技术将内部的 buffer bytes 转换为字符串。所以 String() 方法在转换的时候节省了时间和空间。

// String returns the accumulated string.
func (b *Builder) String() string {
	return *(*string)(unsafe.Pointer(&b.buf))
}

1.7Len方法

// Len returns the number of accumulated bytes; b.Len() == len(b.String()).
func (b *Builder) Len() int { return len(b.buf) }

1.8Cap方法

// Cap returns the capacity of the builder's underlying byte slice. It is the
// total space allocated for the string being built and includes any bytes
// already written.
func (b *Builder) Cap() int { return cap(b.buf) }

1.9Reset方法

// Reset resets the Builder to be empty.
func (b *Builder) Reset() {
	b.addr = nil
	b.buf = nil
}

1.10Grow方法

// Grow grows b's capacity, if necessary, to guarantee space for
// another n bytes. After Grow(n), at least n bytes can be written to b
// without another allocation. If n is negative, Grow panics.
func (b *Builder) Grow(n int) {
	b.copyCheck()
	if n < 0 {
		panic("strings.Builder.Grow: negative count")
	}
	if cap(b.buf)-len(b.buf) < n {
		b.grow(n)
	}
}

1.11grow方法

// grow copies the buffer to a new, larger buffer so that there are at least n
// bytes of capacity beyond len(b.buf).
func (b *Builder) grow(n int) {
	buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
	copy(buf, b.buf)
	b.buf = buf
}

1.12copyCheck方法

func (b *Builder) copyCheck() {
	if b.addr == nil {
		// This hack works around a failing of Go's escape analysis
		// that was causing b to escape and be heap allocated.
		// See issue 23382.
		// TODO: once issue 7921 is fixed, this should be reverted to
		// just "b.addr = b".
		b.addr = (*Builder)(noescape(unsafe.Pointer(b)))
	} else if b.addr != b {
		panic("strings: illegal use of non-zero Builder copied by value")
	}
}

2.strings.Builder介绍

与 bytes.Buffer 类似,strings.Builder 也支持 4 类方法将数据写入 builder 中。

func (b *Builder) Write(p []byte) (int, error)

func (b *Builder) WriteByte(c byte) error

func (b *Builder) WriteRune(r rune) (int, error)

func (b *Builder) WriteString(s string) (int, error)

有了它们,用户可以根据输入数据的不同类型(byte 数组,byte, rune 或者 string),选择对应的写入方法。

3.存储原理

根据用法说明,我们通过调用 string.Builder 的写入方法来写入内容,然后通过调用 String() 方法来获取拼接的字符串。那么 string.Builder 是如何组织这些内容的呢?

通过 slice,string.Builder 通过使用一个内部的 slice 来存储数据片段。当开发者调用写入方法的时候,数据实际上是被追加(append)到了其内部的 slice 上。

4.拷贝问题

strings.Builder 不推荐被拷贝。当你试图拷贝 strings.Builder 并写入的时候,你的程序就会崩溃。

你已经知道,strings.Builder 内部通过 slice 来保存和管理内容。slice 内部则是通过一个指针指向实际保存内容的数组。 当我们拷贝了 builder 以后,同样也拷贝了其 slice 的指针。但是它仍然指向同一个旧的数组。当你对源 builder 或者拷贝后的 builder 写入的时候,问题就产生了。另一个 builder 指向的数组内容也被改变了。这就是为什么 strings.Builder 不允许拷贝的原因。

func main() {
	var b1 strings.Builder
	b1.WriteString("ABC")
	b2 := b1
	b2.WriteString("DEF")//出错在这一行,panic: strings: illegal use of non-zero Builder copied by value
}
func main() {
	var b1 strings.Builder
	b1.WriteString("ABC")
	b2 := b1
	fmt.Println(b2.String())//ABC
}
func main() {
	var b1 strings.Builder
	b1.WriteString("ABC")
	b2 := b1
	fmt.Println(b1.String()) //输出:ABC
	fmt.Println(b2.String()) //输出:ABC
	b1.WriteString("DEF")
	fmt.Println(b1.String()) //输出:ABCDEF
	fmt.Println(b2.String()) //输出:ABC
}

但对于一个未写入任何东西的空内容 builder 则是个例外。我们可以拷贝空内容的 builder 而不报错。

func main() {
	var b1 strings.Builder
	b2 := b1
	fmt.Println(b1.String()) //输出空行
	fmt.Println(b2.String()) //输出空行
	b2.WriteString("DEF")
	fmt.Println(b1.String()) //输出空行
	fmt.Println(b2.String()) //输出:DEF
	b1.WriteString("ABC")
	fmt.Println(b1.String()) //输出:ABC
	fmt.Println(b2.String()) //输出:DEF
}

strings.Builder 会在以下方法中检测拷贝操作:

Grow(n int)

Write(p []byte)

WriteRune(r rune)

WriteString(s string)

所以,拷贝并使用下列这些方法是允许的:

func main() {
	// Reset()
	// Len()
	// String()
	var b1 strings.Builder
	b1.WriteString("ABC")
	b2 := b1
	fmt.Println(b2.Len())    // 3
	fmt.Println(b2.String()) // ABC
	b2.Reset()
	b2.WriteString("DEF")
	fmt.Println(b2.String()) // DEF
}

5.不能与nil作比较

6.Grow深入

strings.Builder 是通过其内部的 slice 来储存内容的。当你调用写入方法的时候,新的字节数据就被追加到 slice 上。如果达到了 slice 的容量(capacity)限制,一个新的 slice 就会被分配,然后老的 slice 上的内容会被拷贝到新的 slice 上。当 slice 长度很大时,这个操作就会很消耗资源甚至引起 内存问题。我们需要避免这一情况。

关于 slice,Go 语言提供了 make([]TypeOfSlice, length, capacity) 方法在初始化的时候预定义它的容量。这就避免了因达到最大容量而引起扩容。

strings.Builder 同样也提供了 Grow() 来支持预定义容量。当我们可以预定义我们需要使用的容量时,strings.Builder 就能避免扩容而创建新的 slice 了。

当调用 Grow() 时,我们必须定义要扩容的字节数(n)。 Grow() 方法保证了其内部的 slice 一定能够写入 n 个字节。只有当 slice 空余空间不足以写入 n 个字节时,扩容才有可能发生。

举个例子:

builder 内部 slice 容量为 10。

builder 内部 slice 长度为 5。

当我们调用 Grow(3) => 扩容操作并不会发生。因为当前的空余空间为 5,足以提供 3 个字节的写入。

当我们调用 Grow(7) => 扩容操作发生。因为当前的空余空间为 5,已不足以提供 7 个字节的写入。

关于上面的情形,如果这时我们调用 Grow(7),则扩容之后的实际容量是多少?

17 还是 12?

实际上,是 27。strings.Builder 的 Grow() 方法是通过 current_capacity * 2 + n (n 就是你想要扩充的容量)的方式来对内部的 slice 进行扩容的。所以说最后的容量是 10*2+7 = 27。 当你预定义 strings.Builder 容量的时候还要注意一点。调用 WriteRune() 和 WriteString() 时,rune 和 string 的字符可能不止 1 个字节。因为,你懂的,UTF-8 的原因。

func main() {
	var b1 strings.Builder
	fmt.Println(b1.Len()) //0
	fmt.Println(b1.Cap()) //0
	b1.Grow(3)
	fmt.Println(b1.Len()) //0
	fmt.Println(b1.Cap()) //3
	b1.Grow(1)
	fmt.Println(b1.Len()) //0
	fmt.Println(b1.Cap()) //3
}
func main() {
	a := strings.Builder{}
	a.Grow(11)
	fmt.Println(a.Len()) //0
	fmt.Println(a.Cap()) //11
	a.WriteRune('李')
	a.WriteRune('陆')
	a.WriteRune('豪')
	a.WriteRune('Z')
	a.WriteRune('Z')
	fmt.Println(a.Len()) //11
	fmt.Println(a.Cap()) //11
}

7.不支持并行读写

和 bytes.Buffer 一样,strings.Builder 也不支持并行的读或者写。所以我们们要稍加注意。

可以试一下,通过同时给 strings.Builder 添加 1000 个字符:

通过运行,你会得到不同长度的结果。但它们都不到 1000。

func main() {
	var b strings.Builder
	n := 0
	var wait sync.WaitGroup
	for n < 1000 {
		wait.Add(1)
		go func() {
			b.WriteString("1")
			n++
			wait.Done()
		}()
	}
	wait.Wait()
	fmt.Println(len(b.String()))
	/*
			第一次运行输出:946
		   第二次运行输出:933
		 第三次运行输出:900
	*/
}

到此这篇关于GoLang strings.Builder底层实现方法详解的文章就介绍到这了,更多相关GoLang strings.Builder内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • go zero微服务实战性能优化极致秒杀

    目录 引言 批量数据聚合 降低消息的消费延迟 怎么保证不会超卖 结束语 引言 上一篇文章中引入了消息队列对秒杀流量做削峰的处理,我们使用的是Kafka,看起来似乎工作的不错,但其实还是有很多隐患存在,如果这些隐患不优化处理掉,那么秒杀抢购活动开始后可能会出现消息堆积.消费延迟.数据不一致.甚至服务崩溃等问题,那么后果可想而知.本篇文章我们就一起来把这些隐患解决掉. 批量数据聚合 在SeckillOrder这个方法中,每来一次秒杀抢购请求都往往Kafka中发送一条消息.假如这个时候有一千万的用户同

  • Go 库性能分析工具pprof

    目录 场景 pprof 生成 profile 文件 CPU 性能分析 内存性能分析 分析 profile 文件 && 优化代码 go tool pprof top 命令 list 命令 总结 场景 我们一般没必要过度优化 Go 程序性能.但是真正需要时,Go 提供的 pprof 工具能帮我们快速定位到问题.比如,我们团队之前有一个服务,在本地和测试环境没问题,一到灰度环境,就报 cpu 负载过高,后经排查,发现某处代码死循环了.我把代码简化成如下: // 处理某些业务,真实的代码中这个死循

  • go原生库的中bytes.Buffer用法

    1 bytes.Buffer定义 bytes.Buffer提供可扩容的字节缓冲区,实质是对切片的封装:结构中包含一个64字节的小切片,避免小内存分配: // A Buffer is a variable-sized buffer of bytes with Read and Write methods. // The zero value for Buffer is an empty buffer ready to use. type Buffer struct { buf []byte //

  • go slice不同初始化方式性能及数组比较详解

    目录 正文 各种场景代码 使用benchmark测试 正文 go语言开发中,slice是我们常用的数据类型之一,也是因为它的灵活性,自己也很少使用数组,当然我也知道它的一些特性,不过没有真实的去验证它,因为大多数使用场景没必要对code太过苛刻,但是如果封装作为包为其他逻辑提供使用的时候,我觉得还是要在意这些事的,毕竟作为公共包使用时,也就证明了使用的频率的频繁性.那么有些事还是指的记录一下,上周闲来无事跑一下吧,今天做一下记录 各种场景代码 其实我们也都知道slice的底层逻辑是一个动态数组,

  • Golang高性能持久化解决方案BoltDB数据库介绍

    目录 1. 介绍Bolt 2. 示例 3. 示例分析 4. 总结 1. 介绍Bolt BoltDB是纯Go语言实现的持久化解决方案,保存数据至内存映射文件.称之为持久化解决方案不是数据库,因为数据库这个词有很多额外功能是bolt所不具备的.正是因为缺乏这些功能使得bolt如此优雅.好用. Bolt就是一个Go包.无需在系统中安装,开始编码前也无需配置,什么都不需要,仅需要go get github.com/boltdb/bolt,然后import "github.com/boltdb/bolt&

  • GoFrame基于性能测试得知grpool使用场景

    目录 前言摘要 先说结论 测试性能代码 运行结果 总结 前言摘要 之前写了一篇 grpool goroutine池详解 | 协程管理 收到了大家积极的反馈,今天这篇来做一下grpool的性能测试分析,让大家更好的了解什么场景下使用grpool比较好. 先说结论 grpool相比于goroutine更节省内存,但是耗时更长: 原因也很简单:grpool复用了协程,减少了协程的创建和销毁,减少了内存消耗:也因为协程的复用,总的goroutine数量更少,导致耗时更多. 测试性能代码 开启for循环,

  • web项目中golang性能监控解析

    目录 性能监控 一.web项目(如gin中) 二.单个的go文件如果查看gc 性能监控 一.web项目(如gin中) 1.使用ginpprof import "github.com/DeanThompson/ginpprof" router := gin.Default() ginpprof.Wrap(router) 2.使用pprof 只需要在main.go中引入:_ “net/http/pprof” 访问:127.0.0.1:8080/debug/pprof /debug/ppro

  • GoLang strings.Builder底层实现方法详解

    目录 1.strings.Builder结构体 1.1strings.Builder结构体 1.2Write方法 1.3WriteByte方法 1.4WriteRune方法 1.5.WriteString方法 1.6String方法 1.7Len方法 1.8Cap方法 1.9Reset方法 1.10Grow方法 1.11grow方法 1.12copyCheck方法 2.strings.Builder介绍 3.存储原理 4.拷贝问题 5.不能与nil作比较 6.Grow深入 7.不支持并行读写 1

  • 在 Golang 中实现 Cache::remember 方法详解

    项目需要把部分代码移植到 Golang , 之前用 Laravel 封装的写起来很舒服,在 Golang 里只能自动动手实现. 一开始想的是使用 interface 实现,但是遇到了一个坑, Golang 里的组合是一个虚假的继承 package main import "fmt" type Person interface { Say() Name() } type Parent struct { } func (s *Parent) Say() { fmt.Println(&quo

  • golang的httpserver优雅重启方法详解

    前言 去年在做golangserver的时候,内部比较头疼的就是在线服务发布的时候,大量用户的请求在发布时候会被重连,在那时候也想了n多的方法,最后还是落在一个github上的项目,facebook的一个golang项目grace,那时候简单研究测试了一下可以就直接在内部使用了起来,这段时间突然想起来,又想仔细研究一下这个项目了. 从原理上来说是这样一个过程: 1)发布新的bin文件去覆盖老的bin文件 2)发送一个信号量,告诉正在运行的进程,进行重启 3)正在运行的进程收到信号后,会以子进程的

  • golang 切片截取参数方法详解

    以 s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}为例 0. 建议:做slice截取时建议用两个参数,尤其是从底层数组进行切片操作时,因为这样在进行第一次append操作时,会给切片重新分配空间,这样减少切片对数组的影响. 1. 结论:s = s[low : high : max] 切片的三个参数的切片截取的意义为 low为截取的起始下标(含), high为窃取的结束下标(不含high),max为切片保留的原切片的最大下标(不含max):即新切片从老切片的low

  • golang实现php里的serialize()和unserialize()序列和反序列方法详解

    Golang 实现 PHP里的 serialize() . unserialize() 安装 go get -u github.com/techleeone/gophp/serialize 用法 package main import ( "fmt" "github.com/techleeone/gophp/serialize" ) func main() { str := `a:1:{s:3:"php";s:24:"世界上最好的语言&

  • Python底层封装实现方法详解

    这篇文章主要介绍了Python底层封装实现方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 事实上,python封装特性的实现纯属"投机取巧",之所以类对象无法直接调用私有方法和属性,是因为底层实现时,python偷偷改变了它们的名称. python在底层实现时,将它们的名称都偷偷改成了"_类名__属性(方法)名"的格式 class Person: def setname(self, name): if le

  • golang项目如何上线部署到Linu服务器(方法详解)

    Go作为Google2009年推出的语言,其被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言. 对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率.它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了. 到现在Go的开发已经是完全开放的,并且拥有一个活跃的社区. 下面看下golang项目如何上线部署到Linu服务器上. windows服务器 先本地编译 go build main.go 编译后会在同级目录生成可执行文件

  • Golang实现文件夹的创建与删除的方法详解

    目录 创建文件夹 删除文件和文件夹 小结 学习笔记,写到哪是哪. 接着上一篇对纯文本文件读写操作,主要去实现一些文件夹操作. 创建文件夹 创建文件夹的时候往往要先判断文件夹是否存在. 样例代码如下 package main import ( "bufio" "fmt" "io" "os" ) //判断文件夹是否存在 func HasDir(path string) (bool, error) { _, _err := os.S

  • Golang实现程序优雅退出的方法详解

    目录 1. 背景 2. 常见的几种平滑关闭 2.1 http server 平滑关闭 2.2 gRPC server 平滑关闭 2.3 worker 协程平滑关闭 2.4 实现 io.Closer 接口的自定义服务平滑关闭 2.5 集成其他框架怎么做 1. 背景 项目开发过程中,随着需求的迭代,代码的发布会频繁进行,在发布过程中,如何让程序做到优雅的退出? 为什么需要优雅的退出? 你的 http 服务,监听端口没有关闭,客户的请求发过来了,但处理了一半,可能造成脏数据. 你的协程 worker

  • Golang泛型的使用方法详解

    目录 1. 泛型是什么 2. 泛型的简单使用 2.1. 泛型示例 2.2. 自定义泛型类型 2.3. 调用带泛型的函数 3. 自定义泛型类型的语法 3.1. 内置的泛型类型any和comparable 3.2. 声明一个自定义类型 3.3. 泛型中的"~"符号是什么 4. 泛型的进阶使用 4.1. 泛型与结构体 5. 泛型的限制或缺陷 5.1 无法直接和switch配合使用 1. 泛型是什么 泛型生命周期只在编译期,旨在为程序员生成代码,减少重复代码的编写 在比较两个数的大小时,没有泛

随机推荐