搜索
首页后端开发GolangGo中string转[]byte的陷阱

Go中string转[]byte的陷阱

Nov 27, 2019 pm 03:13 PM
go

Go中string转[]byte的陷阱

1. 背景

package main
import "fmt"
func main() {
s := []byte("")
s1 := append(s, 'a')
s2 := append(s, 'b')
//fmt.Println(s1, "==========", s2)
fmt.Println(string(s1), "==========", string(s2))
}
// 出现个让我理解不了的现象, 注释时候输出是 b ========== b
// 取消注释输出是 [97] ========== [98] a ========== b

2. slice

2.1 内部结构

先抛去注释的这行代码//fmt.Println(s1, "==========", s2),后面在讲。 当输出 b ========== b时,已经不符合预期结果a和b了。我们知道slice内部并不会存储真实的值,而是对数组片段的引用,其内部结构是:

type slice struct {
    data uintptr
    len int
    cap int}

其中data是指向数组元素的指针,len是指slice要引用数组中的元素数量。cap是指要引用数组中(从data指向开始计算)剩余的元素数量,这个数量减去len,就是还能向这个slice(数组)添加多少元素,如果超出就会发生数据的复制。slice的示意图:

s := make([]byte, 5)// 下图

1.jpg

s = s[2:4]  //会重新生成新的slice,并赋值给s。与底层数组的引用也发生了改变

2.jpg

2.2 覆盖前值

回到问题上,由此可以推断出:s := []byte("") 这行代码中的s实际引用了一个 byte 的数组。

其capacity 是32,length是 0:

s := []byte("")
fmt.Println(cap(s), len(s))
//输出: 32 0

关键点在于下面代码s1 := append(s, 'a')中的append,并没有在原slice修改,当然也没办法修改,因为在Go中都是值传递的。当把s传入append函数内时,已经复制出一份s1,然后在s1上追加 a,s1长度是增加了1,但s长度仍然是0:

s := []byte("")
fmt.Println(cap(s), len(s))
s1 := append(s, 'a')
fmt.Println(cap(s1), len(s1))
// 输出
// 32 0
// 32 1

由于s,s1指向同一份数组,所以在s1上进行append a操作时(底层数组[0]=a),也是s所指向数组的操作,但s本身不会有任何变化。这也是Go中append的写法都是:

s = append(s,'a')

append函数会返回s1,需要重新赋值给s。 如果不赋值的话,s本身记录的数据就滞后了,再次对其append,就会从滞后的数据开始操作。虽然看起是append,实际上确是把上一次append的值给覆盖了。

所以问题的答案是:后append的b,把上次append的a给覆盖了,所以才会输出b b。

假设底层数组是arr,如注释:

s := []byte("")
s1 := append(s, 'a') // 等同于 arr[0] = 'a'
s2 := append(s, 'b') // 等同于 arr[0] = 'b'
fmt.Println(string(s1), "==========", string(s2)) // 只是把同一份数组打印出来了

3. string

3.1 重新分配

老湿,能不能再给力一点?可以,我们继续,先来看个题:

s := []byte{}
s1 := append(s, 'a') 
s2 := append(s, 'b') 
fmt.Println(string(s1), ",", string(s2))
fmt.Println(cap(s), len(s))

猜猜输出什么?

答案是:a , b 和 0 0,符合预期。

上面2.2章节例子中输出的是:32,0。看来问题关键在这里,两者差别在于一个是默认[]byte{},另外个是空字符串转的[]byte("")。其长度都是0,比较好理解,但为什么容量是32就不符合预期输出了?

因为 capacity 是数组还能添加多少的容量,在能满足的情况,不会重新分配。所以 capacity-length=32,是足够appenda,b的。我们用make来验证下:

// append 内会重新分配,输出a,b
s := make([]byte, 0, 0)
// append 内不会重新分配,输出b,b,因为容量为1,足够append
s := make([]byte, 0, 1)
s1 := append(s, 'a')
s2 := append(s, 'b')
fmt.Println(string(s1), ",", string(s2))

重新分配指的是:append 会检查slice大小,如果容量不够,会重新创建个更大的slice,并把原数组复制一份出来。在make([]byte,0,0)这样情况下,s容量肯定不够用,所以s1,s2使用的都是各自从s复制出来的数组,结果也自然符合预期a,b了。

测试重新分配后的容量变大,打印s1:

s := make([]byte, 0, 0)
s1 := append(s, 'a')
fmt.Println(cap(s1), len(s1))
// 输出 8,1。重新分配后扩大了

3.2 二者转换

那为什么空字符串转的slice的容量是32?而不是0或者8呢?

只好祭出杀手锏了,翻源码。Go官方提供的工具,可以查到编译后调用的汇编信息,不然在大片源码中搜索也很累。

-gcflags 是传递参数给Go编译器,-S -S是打印汇编调用信息和数据,-S只打印调用信息。

go run -gcflags '-S -S' main.go

下面是输出:

    0x0000 00000 ()    TEXT    "".main(SB), $264-0
    0x003e 00062 ()   MOVQ    AX, (SP)
    0x0042 00066 ()   XORPS   X0, X0
    0x0045 00069 ()   MOVUPS  X0, 8(SP)
    0x004a 00074 ()   PCDATA  $0, $0
    0x004a 00074 ()   CALL    runtime.stringtoslicebyte(SB)
    0x004f 00079 ()   MOVQ    32(SP), AX
    b , b

Go使用的是plan9汇编语法,虽然整体有些不好理解,但也能看出我们需要的关键点:

CALL    runtime.stringtoslicebyte(SB)

定位源码到src\runtime\string.go:

从stringtoslicebyte函数中可以看出容量32的源头,见注释:

const tmpStringBufSize = 32
type tmpBuf [tmpStringBufSize]byte
func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte  
    if buf != nil && len(s) <= len(buf) {
        *buf = tmpBuf{}   // tmpBuf的默认容量是32
        b = buf[:len(s)]  // 创建个容量为32,长度为0的新slice,赋值给b。
    } else {
        b = rawbyteslice(len(s))
    }
    copy(b, s)  // s是空字符串,复制过去也是长度0
    return b
}

那为什么不是走else中rawbyteslice函数?

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
}

如果走else的话,容量就不是32了。假如走的话,也不影响得出的结论(覆盖),可以测试下:

    s := []byte(strings.Repeat("c", 33))
    s1 := append(s, &#39;a&#39;)
    s2 := append(s, &#39;b&#39;)
    fmt.Println(string(s1), ",", string(s2))
    // cccccccccccccccccccccccccccccccccb , cccccccccccccccccccccccccccccccccb

4. 逃逸分析

老湿,能不能再给力一点?什么时候该走else?老湿你说了大半天,坑还没填,为啥加上注释就符合预期输出a,b? 还有加上注释为啥连容量都变了?

s := []byte("")
fmt.Println(cap(s), len(s))
s1 := append(s, &#39;a&#39;) 
s2 := append(s, &#39;b&#39;) 
fmt.Println(s1, ",", s2)
fmt.Println(string(s1), ",", string(s2))
//输出
// 0 0
// [97] ========== [98]
// a , b

如果用逃逸分析来解释的话,就比较好理解了,先看看什么是逃逸分析。

4.1 提高性能

如果一个函数或子程序内有局部对象,返回时返回该对象的指针,那这个指针可能在任何其他地方会被引用,就可以说该指针就成功“逃逸”了 。 而逃逸分析(escape analysis)就是分析这类指针范围的方法,这样做的好处是提高性能:

最大的好处应该是减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除。

因为逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好

同步消除,如果定义的对象的方法上有同步锁,但在运行时,却只有一个线程在访问,此时逃逸分析后的机器码,会去掉同步锁运行。

Go在编译的时候进行逃逸分析,来决定一个对象放栈上还是放堆上,不逃逸的对象放栈上,可能逃逸的放堆上 。(推荐:go视频教程

4.2 逃到堆上

取消注释情况下:Go编译程序进行逃逸分析时,检测到fmt.Println有引用到s,所以在决定堆上分配s下的数组。在进行string转[]byte时,如果分配到栈上就会有个默认32的容量,分配堆上则没有。

用下面命令执行,可以得到逃逸信息,这个命令只编译程序不运行,上面用的go run -gcflags是传递参数到编译器并运行程序。

go tool compile -m main.go

取消注释fmt.Println(s1, ",", s2) 后 ([]byte)("")会逃逸到堆上:

main.go:23:13: s1 escapes to heap
main.go:20:13: ([]byte)("") escapes to heap  // 逃逸到堆上
main.go:23:18: "," escapes to heap
main.go:23:18: s2 escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:26: "," escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:23:13: main ... argument does not escape
main.go:24:13: main ... argument does not escape

加上注释//fmt.Println(s1, ",", s2)不会逃逸到堆上:

go tool compile -m main.go
main.go:24:20: string(s1) escapes to heap
main.go:24:20: string(s1) escapes to heap
main.go:24:26: "," escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:24:37: string(s2) escapes to heap
main.go:20:13: main ([]byte)("") does not escape  //不逃逸
main.go:24:13: main ... argument does not escape

4.3 逃逸分配

接着继续定位调用stringtoslicebyte的地方,在src\cmd\compile\internal\gc\walk.go 文件。 为了便于理解,下面代码进行了汇总:

const (
    EscUnknown        = iota
    EscNone           // 结果或参数不逃逸堆上.
 )  
case OSTRARRAYBYTE:
        a := nodnil()   //默认数组为空
        if n.Esc == EscNone {
            // 在栈上为slice创建临时数组
            t := types.NewArray(types.Types[TUINT8], tmpstringbufsize)
            a = nod(OADDR, temp(t), nil)
        }
        n = mkcall("stringtoslicebyte", n.Type, init, a, conv(n.Left, types.Types[TSTRING]))

不逃逸情况下会分配个32字节的数组 t。逃逸情况下不分配,数组设置为 nil,所以s的容量是0。接着从s上append a,b到s1,s2,其必然会发生复制,所以不会发生覆盖前值,也符合预期结果a,b 。再看stringtoslicebyte就很清晰了。

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
}

4.4 大小分配

不逃逸情况下默认32。那逃逸情况下分配策略是?

s := []byte("a")
fmt.Println(cap(s))
s1 := append(s, &#39;a&#39;)
s2 := append(s, &#39;b&#39;)
fmt.Print(s1, s2)

如果是空字符串它的输出:0。”a“字符串时输出:8。

大小取决于src\runtime\size.go 中的roundupsize 函数和 class_to_size 变量。

这些增加大小的变化,是由 src\runtime\mksizeclasses.go生成的。

5. 版本差异

老湿,能不能再给力一点? 老湿你讲的全是错误的,我跑的结果和你是反的。对,你没错,作者也没错,毕竟我们在用Go写程序,如果Go底层发生变化了,肯定结果不一样。作者在调研过程中,发现另外博客得到的stringtoslicebyte源码是:

func stringtoslicebyte(s String) (b Slice) {
    b.array = runtime·mallocgc(s.len, 0, FlagNoScan|FlagNoZero);
    b.len = s.len;
    b.cap = s.len;
    runtime·memmove(b.array, s.str, s.len);
}

上面版本的源码,得到的结果,也是符合预期的,因为不会默认分配32字节的数组。

继续翻旧版代码,到1.3.2版是这样:

func stringtoslicebyte(s String) (b Slice) {
    uintptr cap;
    cap = runtime·roundupsize(s.len);
    b.array = runtime·mallocgc(cap, 0, FlagNoScan|FlagNoZero);
    b.len = s.len;
    b.cap = cap;
    runtime·memmove(b.array, s.str, s.len);
    if(cap != b.len)
        runtime·memclr(b.array+b.len, cap-b.len);
}

1.6.4版:

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte
    if buf != nil && len(s) <= len(buf) {
        b = buf[:len(s):len(s)]
    } else {
        b = rawbyteslice(len(s))
    }
    copy(b, s)
    return b
}

更古老的:

struct __go_open_array
__go_string_to_byte_array (String str)
{
  uintptr cap;
  unsigned char *data;
  struct __go_open_array ret;

  cap = runtime_roundupsize (str.len);
  data = (unsigned char *) runtime_mallocgc (cap, 0, FlagNoScan | FlagNoZero);
  __builtin_memcpy (data, str.str, str.len);
  if (cap != (uintptr) str.len)
    __builtin_memset (data + str.len, 0, cap - (uintptr) str.len);
  ret.__values = (void *) data;
  ret.__count = str.len;
  ret.__capacity = str.len;
  return ret;
}

总结下:

注释时输出b,b。是因为没有逃逸,所以分配了默认32字节大小的数组,2次append都是在数组[0]赋值,后值覆盖前值,所以才是b,b。

消注释时输出a,b。是因为fmt.Println引用了s,逃逸分析时发现需要逃逸并且是空字符串,所以分配了空数组。2次append都是操作各自重新分配后的新slice,所以输出a,b。

更多go语言相关知识请关注go语言教程栏目。

以上是Go中string转[]byte的陷阱的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文转载于:博客园。如有侵权,请联系admin@php.cn删除
实施静音和锁以寻求线程安全性实施静音和锁以寻求线程安全性May 05, 2025 am 12:18 AM

在Go中,使用互斥锁和锁是确保线程安全的关键。1)使用sync.Mutex进行互斥访问,2)使用sync.RWMutex处理读写操作,3)使用原子操作进行性能优化。掌握这些工具及其使用技巧对于编写高效、可靠的并发程序至关重要。

基准测试和分析并发GO代码基准测试和分析并发GO代码May 05, 2025 am 12:18 AM

如何优化并发Go代码的性能?使用Go的内置工具如gotest、gobench和pprof进行基准测试和性能分析。1)使用testing包编写基准测试,评估并发函数的执行速度。2)通过pprof工具进行性能分析,识别程序中的瓶颈。3)调整垃圾收集设置以减少其对性能的影响。4)优化通道操作和限制goroutine数量以提高效率。通过持续的基准测试和性能分析,可以有效提升并发Go代码的性能。

并发程序中的错误处理:避免常见的陷阱并发程序中的错误处理:避免常见的陷阱May 05, 2025 am 12:17 AM

避免并发Go程序中错误处理的常见陷阱的方法包括:1.确保错误传播,2.处理超时,3.聚合错误,4.使用上下文管理,5.错误包装,6.日志记录,7.测试。这些策略有助于有效处理并发环境中的错误。

隐式接口实现:鸭打字的力量隐式接口实现:鸭打字的力量May 05, 2025 am 12:14 AM

IndimitInterfaceImplementationingingoembodiesducktybybyallowingTypestoSatoSatiSatiSatiSatiSatiSatsatSatiSatplicesWithouTexpliclIctDeclaration.1)itpromotesflemotesflexibility andmodularitybybyfocusingion.2)挑战挑战InclocteSincludeUpdatingMethodSignateSignatiSantTrackingImplections.3)工具li

进行错误处理:最佳实践和模式进行错误处理:最佳实践和模式May 04, 2025 am 12:19 AM

在Go编程中,有效管理错误的方法包括:1)使用错误值而非异常,2)采用错误包装技术,3)定义自定义错误类型,4)复用错误值以提高性能,5)谨慎使用panic和recover,6)确保错误消息清晰且一致,7)记录错误处理策略,8)将错误视为一等公民,9)使用错误通道处理异步错误。这些做法和模式有助于编写更健壮、可维护和高效的代码。

您如何在GO中实施并发?您如何在GO中实施并发?May 04, 2025 am 12:13 AM

在Go中实现并发可以通过使用goroutines和channels来实现。1)使用goroutines来并行执行任务,如示例中同时享受音乐和观察朋友。2)通过channels在goroutines之间安全传递数据,如生产者和消费者模式。3)避免过度使用goroutines和死锁,合理设计系统以优化并发程序。

在GO中构建并发数据结构在GO中构建并发数据结构May 04, 2025 am 12:09 AM

Gooffersmultipleapproachesforbuildingconcurrentdatastructures,includingmutexes,channels,andatomicoperations.1)Mutexesprovidesimplethreadsafetybutcancauseperformancebottlenecks.2)Channelsofferscalabilitybutmayblockiffullorempty.3)Atomicoperationsareef

将GO的错误处理与其他编程语言进行比较将GO的错误处理与其他编程语言进行比较May 04, 2025 am 12:09 AM

go'serrorhandlingisexplicit,治疗eRROSASRETRATERTHANEXCEPTIONS,与pythonandjava.1)go'sapphifeensuresererrawaresserrorawarenessbutcanleadtoverbosecode.2)pythonandjavauseexeexceptionseforforforforforcleanerCodebutmaymobisserrors.3)

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版

MinGW - 适用于 Windows 的极简 GNU

MinGW - 适用于 Windows 的极简 GNU

这个项目正在迁移到osdn.net/projects/mingw的过程中,你可以继续在那里关注我们。MinGW:GNU编译器集合(GCC)的本地Windows移植版本,可自由分发的导入库和用于构建本地Windows应用程序的头文件;包括对MSVC运行时的扩展,以支持C99功能。MinGW的所有软件都可以在64位Windows平台上运行。

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),

螳螂BT

螳螂BT

Mantis是一个易于部署的基于Web的缺陷跟踪工具,用于帮助产品缺陷跟踪。它需要PHP、MySQL和一个Web服务器。请查看我们的演示和托管服务。

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境