刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • go语言基础

  • go语言进阶

  • go语言实现原理

    • channel 实现原理
    • string 实现原理
      • string 数据结构
      • string 操作
        • 1. 字符串的构建
        • 2. []byte转string
        • 3. string转[]byte
      • 字符串不允许修改
      • []byte转string一定会拷贝内存吗?
      • string和[]byte取舍
      • string 的高效拼接
        • 1. 推荐方式: strings.Builder
        • 2. 原理
        • a. string.Builder 和 +
        • b. strings.Builder 和 bytes.Buffer
    • iota
    • slice 实现原理
    • map 实现原理
    • sync.Map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁 CAS 实现原理
    • defer实现原理
    • singleflight实现原理
    • timerate令牌桶限流 实现原理
  • gin框架

  • gorm

  • go测试

  • Go语言
  • go语言实现原理
bigox
2022-06-17
目录

string 实现原理

# string 数据结构

  • string是引用类型

  • string可以为空(长度为0),但不会是nil;

  • string对象不可以修改。

  • src/builtin/builtin.go

    type stringStruct struct {
        str unsafe.Pointer
        len int
    }
    
    1
    2
    3
    4

# string 操作

# 1. 字符串的构建

  • 字符串构建过程是先根据字符串构建stringStruct,再转换成string。转换的源码如下:

    func gostringnocopy(str *byte) string { // 根据字符串地址构建string
        ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)} // 先构造stringStruct
        s := *(*string)(unsafe.Pointer(&ss))                             // 再将stringStruct转换成string
        return s
    }
    
    1
    2
    3
    4
    5

# 2. []byte转string

  • code

    func GetStringBySlice(s []byte) string {
        return string(s)
    }
    
    1
    2
    3
  • 需要注意的是这种转换需要一次内存拷贝

    • 根据切片的长度申请内存空间,假设内存地址为p,切片长度为len(b);

    • 构建string(string.str = p;string.len = len;)

    • 拷贝数据(切片中数据拷贝到新申请的内存空间)

      image-20220614163118267

# 3. string转[]byte

  • code

    func GetSliceByString(str string) []byte {
        return []byte(str)
    }
    
    1
    2
    3
  • 需要注意的是这种转换需要一次内存拷贝

    • 申请切片内存空间

    • 将string拷贝到切片

      image-20220614163210111

# 字符串不允许修改

  • 像C++语言中的string,其本身拥有内存空间,修改string是支持的。但Go的实现中,string不包含内存空间,只有一个内存的指针,这样做的好处是string变得非常轻量,可以很方便的进行传递而不用担心内存拷贝。

  • 因为string通常指向字符串字面量,而字符串字面量存储位置是只读段,而不是堆或栈上,所以才有了string不可修改的约定。

# []byte转string一定会拷贝内存吗?

  • byte切片转换成string的场景很多,为了性能上的考虑,有时候只是临时需要字符串的场景下,byte切片转换成string时并不会拷贝内存,而是直接返回一个string,这个string的指针(string.str)指向切片的内存。

  • 比如,编译器会识别如下临时场景:

    • 使用m[string(b)]来查找map(map是string为key,临时把切片b转成string);

    • 字符串拼接,如”<” + “string(b)” + “>”;

    • 字符串比较:string(b) == “foo”

  • 因为是临时把byte切片转换成string,也就避免了因byte切片同容改成而导致string引用失败的情况,所以此时可以不必拷贝内存新建一个string。

# string和[]byte取舍

  • string和[]byte都可以表示字符串,但因数据结构不同,其衍生出来的方法也不同,要根据实际应用场景来选择。

  • string 擅长的场景:

    • 需要字符串比较的场景;

    • 不需要nil字符串的场景;

  • []byte擅长的场景:

    • 修改字符串的场景,尤其是修改粒度为1个字节;

    • 函数返回值,需要用nil表示含义的场景;

  • 需要切片操作的场景;

    • 虽然看起来string适用的场景不如[]byte多,但因为string直观,在实际应用中还是大量存在,在偏底层的实现中[]byte使用更多

# string 的高效拼接

速度 6>5>4>3>1>2

  1. 使用 +

    // +
    func plusConcat(n int, str string) string {
    	s := ""
    	for i := 0; i < n; i++ {
    		s += str
    	}
    	return s
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  2. 使用 fmt.Sprintf

    // Sprintf
    func sprintfConcat(n int, str string) string {
       s := ""
       for i := 0; i < n; i++ {
          s = fmt.Sprintf("%s%s", s, str)
       }
       return s
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 使用 strings.Builder

    // strings.Builder
    func builderConcat(n int, str string) string {
    	var builder strings.Builder
    	for i := 0; i < n; i++ {
    		builder.WriteString(str)
    	}
    	return builder.String()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  4. 使用 bytes.Buffer

    // bytes.Buffer
    func bufferConcat(n int, s string) string {
    	buf := new(bytes.Buffer)
    	for i := 0; i < n; i++ {
    		buf.WriteString(s)
    	}
    	return buf.String()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  5. 使用 []byte

    //[]byte
    func byteConcat(n int, str string) string {
    	buf := make([]byte, 0)
    	for i := 0; i < n; i++ {
    		buf = append(buf, str...)
    	}
    	return string(buf)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  6. 带有容量的 []byte

    func preByteConcat(n int, str string) string {
    	buf := make([]byte, 0, n*len(str))
    	for i := 0; i < n; i++ {
    		buf = append(buf, str...)
    	}
    	return string(buf)
    }
    
    1
    2
    3
    4
    5
    6
    7
  • 测试用例

    package main
    
    import "testing"
    
    func benchmark(b *testing.B, f func(int, string) string) {
    	var str = randomString(10)
    	for i := 0; i < b.N; i++ {
    		f(10000, str)
    	}
    }
    func BenchmarkPlusConcat(b *testing.B)    { benchmark(b, plusConcat) }
    func BenchmarkSprintfConcat(b *testing.B) { benchmark(b, sprintfConcat) }
    func BenchmarkBuilderConcat(b *testing.B) { benchmark(b, builderConcat) }
    func BenchmarkBufferConcat(b *testing.B)  { benchmark(b, bufferConcat) }
    func BenchmarkByteConcat(b *testing.B)    { benchmark(b, byteConcat) }
    func BenchmarkPreByteConcat(b *testing.B) { benchmark(b, preByteConcat) }
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
  • 基准测试结果: 6>5>4>3>1>2

    go test -bench="Concat$" -benchmem .

    goos: darwin
    goarch: arm64
    pkg: pic
    BenchmarkPlusConcat-8                 43          28540977 ns/op        530996174 B/op     10009 allocs/op
    BenchmarkSprintfConcat-8              22          50766222 ns/op        833881447 B/op     37392 allocs/op
    BenchmarkBuilderConcat-8           17703             71768 ns/op          505841 B/op         24 allocs/op
    BenchmarkBufferConcat-8            19694             60595 ns/op          423537 B/op         13 allocs/op
    BenchmarkByteConcat-8              21356             56174 ns/op          612337 B/op         25 allocs/op
    BenchmarkPreByteConcat-8           36676             31380 ns/op          212992 B/op          2 allocs/op
    PASS
    ok      pic     10.566s
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    • 从基准测试的结果来看,使用 + 和 fmt.Sprintf 的效率是最低的,和其余的方式相比,性能相差约 1000 倍,而且消耗了超过 1000 倍的内存。当然 fmt.Sprintf 通常是用来格式化字符串的,一般不会用来拼接字符串。

    • strings.Builder、bytes.Buffer 和 []byte 的性能差距不大,而且消耗的内存也十分接近,性能最好且消耗内存最小的是 preByteConcat,这种方式预分配了内存,在字符串拼接的过程中,不需要进行字符串的拷贝,也不需要分配新的内存,因此性能最好,且内存消耗最小。

# 1. 推荐方式: strings.Builder

  • 这是 Go 官方对 strings.Builder 的解释:

    A Builder is used to efficiently build a string using Write methods. It minimizes memory copying.

  • string.Builder 也提供了预分配内存的方式 Grow:

    func builderConcat(n int, str string) string {
    var builder strings.Builder
    builder.Grow(n * len(str))
    for i := 0; i < n; i++ {
    	builder.WriteString(str)
    }
    return builder.String()
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 使用了 Grow 优化后的版本的 benchmark 结果如下:

    BenchmarkBuilderConcat-8   16855    0.07 ns/op   0.1 MB/op       1 allocs/op
    BenchmarkPreByteConcat-8   17379    0.07 ms/op   0.2 MB/op       2 allocs/op
    
    1
    2
  • 与预分配内存的 []byte 相比,因为省去了 []byte 和字符串(string) 之间的转换,内存分配次数还减少了 1 次,内存消耗减半。

# 2. 原理

# a. string.Builder 和 +

  • 字符串在 Go 语言中是不可变类型,占用内存大小是固定的,当使用 + 拼接 2 个字符串时,生成一个新的字符串,那么就需要开辟一段新的空间,新空间的大小是原来两个字符串的大小之和。拼接第三个字符串时,再开辟一段新空间,新空间大小是三个字符串大小之和,以此类推。假设一个字符串大小为 10 byte,拼接 1w 次,需要申请的内存大小为:

    10 + 2 * 10 + 3 * 10 + ... + 10000 * 10 byte = 500 MB 
    
    1
  • 而 strings.Builder,bytes.Buffer,包括切片 []byte 的内存是以倍数申请的。例如,初始大小为 0,当第一次写入大小为 10 byte 的字符串时,则会申请大小为 16 byte 的内存(恰好大于 10 byte 的 2 的指数),第二次写入 10 byte 时,内存不够,则申请 32 byte 的内存,第三次写入内存足够,则不申请新的,以此类推。在实际过程中,超过一定大小,比如 2048 byte 后,申请策略上会有些许调整。

    • 2048 以前按倍数申请,2048 之后,以 640 递增,最后一次递增 24576 到 122880。总共申请的内存大小约 0.52 MB,约为上一种方式的千分之一。

# b. strings.Builder 和 bytes.Buffer

  • strings.Builder 和 bytes.Buffer 底层都是 []byte 数组,但 strings.Builder 性能比 bytes.Buffer 略快约 10% 。一个比较重要的区别在于,bytes.Buffer 转化为字符串时重新申请了一块空间,存放生成的字符串变量,而 strings.Builder 直接将底层的 []byte 转换成了字符串类型返回了回来。

    • bytes.Buffer
    // To build strings more efficiently, see the strings.Builder type.
    func (b *Buffer) String() string {
    	if b == nil {
    		// Special case, useful in debugging.
    		return "<nil>"
    	}
    	return string(b.buf[b.off:])
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    • strings.Builder
    // String returns the accumulated string.
    func (b *Builder) String() string {
    	return *(*string)(unsafe.Pointer(&b.buf))
    }
    
    1
    2
    3
    4
#Go#
上次更新: 2023/04/16, 18:35:33
channel 实现原理
iota

← channel 实现原理 iota→

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式