Golang 源码解读 01、深入解析 strings.Builder、strings.Join

本文从我的《The Go Programming Language》学习笔记中分离出,单独成一篇文章方便查阅参考。

strings.Builder 源码解析

存在意义

使用 (strings.Builder),避免频繁创建字符串对象,进而提高性能

(Source\ file) https://go.dev/src/strings/builder.go

与许多支持 (string) 类型的语言一样,(golang) 中的 (string) 类型也是只读且不可变的( (string) 类型笔记 Go xmas2020 学习笔记 04、Strings – 小能日记 – 博客园 )。因此,通过循环字符串切片拼接字符串的方式会导致大量的string创建、销毁和内存分配。如果你拼接的字符串比较多的话,这显然不是一个正确的姿势。

在 (strings.Builder) 出来以前,我们是用 (bytes.Buffer) 来进行优化的。

func main() {
    ss := []string{
        "A",
        "B",
        "C",
    }

    var b bytes.Buffer
    for _, s := range ss {
        fmt.Fprint(&b, s)
    }

    print(b.String())
}

这里使用 var b bytes.Buffer 存放最终拼接好的字符串,一定程度上避免上面 (str) 每进行一次拼接操作就重新申请新的内存空间存放中间字符串的问题。

但这里依然有一个小问题: (b.String()) 会有一次 ([\ ]byte -> string) 类型转换。而这个操作是会进行一次内存分配和内容拷贝的。

实现原理

Golang 官方将 (strings.Builder) 作为一个(feature) 引入。

  1. 与(byte.Buffer) 思路类似,既然(string) 在构建过程中会不断的被销毁重建,那么就尽量避免这个问题,底层使用一个(buf\ [\ ]byte) 来存放字符串的内容。
  2. 对于写操作,就是简单的将(byte) 写入到(buf) 即可。
  3. 为了解决(bytes.Buffer.String()) 存在的([\ ]byte -> string) 类型转换和内存拷贝问题,这里使用了一个(unsafe.Pointer) 的内存指针转换操作,实现了直接将(buf\ [\ ]byte)转换为(string) 类型,同时避免了内存充分配的问题。
  4. 如果我们自己来实现(strings.Builder) , 大部分情况下我们完成前3步就觉得大功告成了。但是标准库做得要更近一步。我们知道 Golang 的堆栈在大部分情况下是不需要开发者关注的,如果能够在栈上完成的工作逃逸到了堆上,性能就大打折扣了。因此,(copyCheck) 加入了一行比较(hack) 的代码来避免(buf) 逃逸到堆上。Go 栈、堆的知识可看GopherCon SG 2019 “Understanding Allocations” 学习笔记 – 小能日记 – 博客园

常用方法

  • String方法返回Builder构建的数据
  • Len方法返回字节数组占据的字节数,1个汉字三个字节
  • Cap方法返回字节数组分配的内存空间大小
  • Reset方法将Builder重置为初始状态
  • Write方法将字节数组加添加到buf数组后面
  • WriteByte将字节c添加到buf数组后边
  • WriteRune将rune字符添加到buf数组后面
  • WriteString将字符串添加到buf数组后面

写入方法

(bytes.Buffer) 也支持这四个写入方法。

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)

Golang 源码解读 01、深入解析 strings.Builder、strings.Join

strings.Builder organizes the content based on the internal slice to organize. When you call write-methods, they append new bytes to inner-slice. If the slice’s capacity is reached, Go will allocate a new slice with different memory space and copy old slice to a new one. It will take resource to do when the slice is large or it may create the memory issue
The rune and a character of string can be more than 1 bytes when you WriteRune() or WriteString()

我们可以预定义切片的容量来避免重新分配。

扩容方法

追加内容也有讲究,因为底层是 (slice),追加数据时有可能引起 (slice) 扩容。一般的优化方案是为 (slice) 初始化合理的空间,避免多次扩容复制。(Builder) 也提供了预分配内存的方法,如 (Grow) 方法。

func (b *Builder) grow(n int) {
    buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
    copy(buf, b.buf)
    b.buf = buf
}

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

注意扩容的容量和 (slice) 直接扩容两倍的方式略有不同,它是 2*cap(b.buf)+n,之前容量的两倍加 (n)。

  • 如果容量是10,长度是5,调用(Grow(3))结果是什么?当前容量足够使用,没有任何操作;
  • 如果容量是10,长度是5,调用(Grow(7))结果是什么?剩余空间是5,不满足7个扩容空间,底层需要扩容。扩容的时候按照之前容量的两倍再加(n) 的新容量扩容,结果是(2*10+7=27)。

String() 方法

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

先获取 ([\ ]byte) 地址,然后转成字符串指针,然后再取地址。

从 ptype 输出的结构来看,string 可看做 [2]uintptr,而 [ ]byte 则是 [3]uintptr,这便于我们编写代码,无需额外定义结构类型。如此,str2bytes 只需构建 [3]uintptr{ptr, len, len},而 bytes2str 更简单,直接转换指针类型,忽略掉 cap 即可。

禁止复制

type Builder struct {
    addr *Builder // of receiver, to detect copies by value
    buf  []byte
}

(Builder) 的底层数据,它还有个字段 (addr) ,是一个指向 (Builder) 的指针。默认情况是它会指向自己。

b.addr = (*Builder)(noescape(unsafe.Pointer(b)))

(copyCheck) 用来 保证复制后不允许修改的逻辑。仔细看下源码,如果 (addr) 是空,也就是没有数据的时候是可以被复制后修改的,一旦那边有数据了,就不能这么搞了。在 (Grow)、(Write)、(WriteByte)、(WriteString)、(WriteRune) 这五个函数里都有这个检查逻辑。

var b1 strings.Builder
b2 := b1
b2.WriteString("DEF")
b1.WriteString("ABC")
// b1 = ABC, b2 = DEF
var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
b2.WriteString("DEF")

代码将会报错 illegal use of non-zero Builder copied by value

下面的意思是拷贝过来的Builder进行添加修改,会造成其他Builder的修改。

When we copy the Builder, we clone the pointer of the slice but they still point to the old array. The problem will be occurs when you try to Write something to copied Builder or source Builder, the other’s content will be affects. That’s reason why strings.Builder prevent copy actions.

Golang 源码解读 01、深入解析 strings.Builder、strings.Join

我们可以使用 (Reset) 方法对 (addr、buf) 置空。下面拷贝了使用 (Reset) 后不会报错。

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

线程不安全

func main() {
    var b strings.Builder
    var n int32
    var wait sync.WaitGroup
    for i := 0; i < 1000; i++ {
        wait.Add(1)
        go func() {
            atomic.AddInt32(&n, 1)
            b.WriteString("1")
            wait.Done()
        }()
    }
    wait.Wait()
    fmt.Println(len(b.String()), n)
}
905 1000

结果是 (905\ 1000),并不都是 (1000) 。如果想保证线程安全,需要在 (WriteString) 的时候加锁。

io.Writer 接口

(strings.Builder) 实现了 (io.Writer) 接口。可以使用在很多例子中

  • io.Copy(dst Writer, src Reader) (written int64, err error)
  • bufio.NewWriter(w io.Writer) *Writer
  • fmt.Fprint(w io.Writer, a …interface{}) (n int, err error)
  • func (r *http.Request) Write(w io.Writer) error
  • and other libraries that uses (io.Writer)

代码

func main() {
    var b strings.Builder
    fmt.Printf("%v", b)
    fmt.Println(b.Len(), b.Cap())
    for i := 3; i >= 1; i-- {
        fmt.Fprintf(&b, "%d#", i)
        fmt.Printf("%v\n", b)
        fmt.Println(b.Len(), b.Cap())
    }
    b.WriteString("Hello")
    fmt.Printf("%v\n", b)
    fmt.Println(b.Len(), b.Cap())
    fmt.Println(b.String())

    // b.Grow(5) // ^ 扩容
    b.Grow(88) // ^ 扩容
    fmt.Printf("%v\n", b)
    fmt.Println(b.Len(), b.Cap())

    fmt.Println(unsafeEqual("Hello", []byte{72, 101, 108, 108, 111}))
}

func unsafeEqual(a string, b []byte) bool {
    bbp := *(*string)(unsafe.Pointer(&b))
    return a == bbp
}
{<nil> []}0 0
{0xc0001223a0 [51 35]}
2 8
{0xc0001223a0 [51 35 50 35]}
4 8
{0xc0001223a0 [51 35 50 35 49 35]}
6 8
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 16
3#2#1#Hello
{0xc0001223a0 [51 35 50 35 49 35 72 101 108 108 111]}
11 120
true
</nil>

strings.Join 源码解析

实现原理

// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.

func Join(elems []string, sep string) string {
    switch len(elems) {
    case 0:
        return ""
    case 1:
        return elems[0]
    }
    n := len(sep) * (len(elems) - 1)
    for i := 0; i < len(elems); i++ {
        n += len(elems[i])
    }

    var b Builder
    b.Grow(n)
    b.WriteString(elems[0])
    for _, s := range elems[1:] {
        b.WriteString(sep)
        b.WriteString(s)
    }
    return b.String()
}

前面计算出整个字符串需要的长度 (n),然后创建 (strings.Builder) 并通过 (Grow) 方法直接扩容,大小为 (02+n) 为 (n) ,然后通过 (WriteString) 方法写入,最后调用 (String) 方法返回字符串。 只构造了一次字符串对象*。

代码

func main() {
    args := make([]string, 10000)
    for i := 0; i < 10000; i++ {
        args = append(args, strconv.Itoa(i+1))
    }
    var s string
    start := time.Now()
    for i := range args {
        s += args[i]
        s += " "
    }
    // fmt.Println(s) // ^ 因为切片数量过大先不打印就只做合并操作
    t := time.Since(start)
    fmt.Println("elapsed time", t)
    /* -------------------------------------------------------------------------- */
    start = time.Now()
    s = strings.Join(args, " ")
    fmt.Println(s)
    t = time.Since(start)
    fmt.Println("elapsed time", t)
}
elapsed time 230.8618ms
elapsed time 0s

Original: https://www.cnblogs.com/linxiaoxu/p/16117562.html
Author: 小能日记
Title: Golang 源码解读 01、深入解析 strings.Builder、strings.Join

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/516372/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Go 语言快速开发入门

    需求 开发的步骤 linux下如何开发Go程序 MAC下如何开发Go程序 Golang执行流程分析 编译和运行说明 Go程序开发的注意事项 Go语言的转义字符(escapechar…

    Go语言 2023年5月25日
    078
  • Go微服务框架go-kratos学习05:分布式链路追踪 OpenTelemetry 使用

    一、分布式链路追踪发展简介 1.1 分布式链路追踪介绍 关于分布式链路追踪的介绍,可以查看我前面的文章 微服务架构学习与思考(09):分布式链路追踪系统-dapper论文学习(ht…

    Go语言 2023年5月25日
    046
  • Golang:将日志以Json格式输出到Kafka

    工程实践中,我们往往还需要对日志进行采集,将日志归集到一起,然后用于各种处理分析,比如生产环境上的错误分析、异常告警等等。在日志消息系统领域,Kafka久负盛名,这篇文章就以将日志…

    Go语言 2023年5月25日
    075
  • Go学习【02】:理解Gin,搭一个web demo

    Go Gin 框架 说Gin是一个框架,不如说Gin是一个类库或者工具库,其包含了可以组成框架的组件。这样会更好理解一点。 举个🌰 端口监听 用于监听请求,也就是服务 请求处理 请…

    Go语言 2023年5月25日
    072
  • Go 学习路线(2022)

    原文链接: Go 学习路线(2022) Go 语言的发展越来越好了,很多大厂使用 Go 作为主要开发语言,也有很多人开始学习 Go,准备转 Go 开发。 那么,怎么学呢? 我发现,…

    Go语言 2023年5月25日
    055
  • GO语言 文件操作实例

    package main import ( "bufio" "fmt" "io/ioutil" "os&quo…

    Go语言 2023年5月29日
    052
  • 听说,99% 的 Go 程序员都被 defer 坑过

    先声明:我被坑过。 出问题就对了,这个小东西坏的很,一不留神就出错。 所以,面对这种情况,我们今天就不讲道理了。直接把我珍藏多年的代码一把梭,凭借多年踩坑经历和写 BUG 经验,我…

    Go语言 2023年5月25日
    069
  • golang常用库包:Go依赖注入(DI)工具-wire使用

    google 出品的依赖注入库 wire:https://github.com/google/wire 什么是依赖注入 依赖注入 ,英文全名是 dependency injecti…

    Go语言 2023年5月25日
    058
  • Go语言之循环与条件判断

    Go 语言中没有 while 循环,只有一个 for 循环 for 变量初始化;条件;变量自增/自减 { 循环体内容 } 1、基本使用 for i := 0; i < 10;…

    Go语言 2023年5月25日
    057
  • go语言学习笔记

    最近一直在学习go语言,因此打算学习的时候能够记录一下笔记。我这个人之前是从来没有记录笔记的习惯,一直以来都是靠强大的记忆力去把一些要点记住。读书的时候因为一直都是有一个很安静和很…

    Go语言 2023年5月29日
    049
  • Go Micro Dashboard – 实现细节(一)

    前言 Go Micro Dashboard是基于go-micro和ng-alain开发的, 它既是go-micro开发过程中的工具,也可以作为学习go-micro的实际案例。接下来…

    Go语言 2023年5月25日
    075
  • Go语言基础笔记

    Go语言基础语法:https://note.youdao.com/noteshare?id=12098a23b490260282f6b7c347ab182f Go语言内建容器:ht…

    Go语言 2023年5月29日
    079
  • Go微服务框架-2.Go语言RPC编程实践

    Go语言实现RPC编程 上节课我们对RPC知识做了介绍,讲解了RPC的原理,通过图示方式讲解了RPC的内部执行过程。本节课,我们继续来学习RPC相关的内容。 在Go语言官方网站的p…

    Go语言 2023年5月29日
    063
  • Golang(go语言)开发环境配置

    VSCode开发环境配置 (1)把vscode安装软件准备好 如果不清楚选64位还是32位可以在我的电脑->右击->点属性->即可查看 (2)双击安装文件就可以一…

    Go语言 2023年5月25日
    054
  • Operator 示例:使用 Redis 部署 PHP 留言板应用程序

    安装 Docker Desktop,并启动内置的 Kubernetes 集群 注册一个hub.docker.com 账户,需要将本地构建好的镜像推送至公开仓库中 安装 operat…

    Go语言 2023年5月25日
    066
  • Go Error 嵌套到底是怎么实现的?

    原文链接: Go Error 嵌套到底是怎么实现的? Go Error 的设计哲学是 「Errors Are Values」。 这句话应该怎么理解呢?翻译起来挺难的。不过从源码的角…

    Go语言 2023年5月25日
    065
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球