golang 的string与[]byte转换方式

相对于C语言,golang是类型安全的语言。但是安全的代价就是性能的妥协。

下面我们看看Golang不想让我们看到的“秘密”——string的底层数据。

通过reflect包,我们可以知道,在Golang底层,string和slice其实都是struct:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
type StringHeader struct {
    Data uintptr
    Len  int
}

其中Data是一个指针,指向实际的数据地址,Len表示数据长度。

但是,在string和[]byte转换过程中,Golang究竟悄悄帮我们做了什么,来达到安全的目的?

在Golang语言规范里面,string数据是禁止修改的,试图通过&s[0], &b[0]取得string和slice数据指针地址也是不能通过编译的。

下面,我们就通过Golang的“黑科技”来一窥Golang背后的“秘密”

//return GoString's buffer slice(enable modify string)
func StringBytes(s string) Bytes {
    return *(*Bytes)(unsafe.Pointer(&s))
}
// convert b to string without copy
func BytesString(b []byte) String {
    return *(*String)(unsafe.Pointer(&b))
}
// returns &s[0], which is not allowed in go
func StringPointer(s string) unsafe.Pointer {
    p := (*reflect.StringHeader)(unsafe.Pointer(&s))
    return unsafe.Pointer(p.Data)
}
// returns &b[0], which is not allowed in go
func BytesPointer(b []byte) unsafe.Pointer {
    p := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    return unsafe.Pointer(p.Data)
}

以上4个函数的神奇之处在于,通过unsafe.Pointer和reflect.XXXHeader取到了数据首地址,并实现了string和[]byte的直接转换(这些操作在语言层面是禁止的)。

下面我们就通过这几个“黑科技”来测试一下语言底层的秘密:

func TestPointer(t *testing.T) {
    s := []string{
        "",
        "",
        "hello",
        "hello",
        fmt.Sprintf(""),
        fmt.Sprintf(""),
        fmt.Sprintf("hello"),
        fmt.Sprintf("hello"),
    }
    fmt.Println("String to bytes:")
    for i, v := range s {
        b := unsafe.StringBytes(v)
        b2 := []byte(v)
        if b.Writeable() {
            b[0] = 'x'
        }
        fmt.Printf("%d\ts=%5s\tptr(v)=%-12v\tptr(StringBytes(v)=%-12v\tptr([]byte(v)=%-12v\n",
            i, v, unsafe.StringPointer(v), b.Pointer(), unsafe.BytesPointer(b2))
    }
    b := [][]byte{
        []byte{},
        []byte{'h', 'e', 'l', 'l', 'o'},
    }
    fmt.Println("Bytes to string:")
    for i, v := range b {
        s1 := unsafe.BytesString(v)
        s2 := string(v)
        fmt.Printf("%d\ts=%5s\tptr(v)=%-12v\tptr(StringBytes(v)=%-12v\tptr(string(v)=%-12v\n",
            i, s1, unsafe.BytesPointer(v), s1.Pointer(), unsafe.StringPointer(s2))
    }
}
const N = 3000000
func Benchmark_Normal(b *testing.B) {
    for i := 1; i < N; i++ {
        s := fmt.Sprintf("12345678901234567890123456789012345678901234567890")
        bb := []byte(s)
        bb[0] = 'x'
        s = string(bb)
        s = s
    }
}
func Benchmark_Direct(b *testing.B) {
    for i := 1; i < N; i++ {
        s := fmt.Sprintf("12345678901234567890123456789012345678901234567890")
        bb := unsafe.StringBytes(s)
        bb[0] = 'x'
        s = s
    }
}
//test result
//String to bytes:
//0 s=      ptr(v)=0x51bd70     ptr(StringBytes(v)=0x51bd70     ptr([]byte(v)=0xc042021c58
//1 s=      ptr(v)=0x51bd70     ptr(StringBytes(v)=0x51bd70     ptr([]byte(v)=0xc042021c58
//2 s=hello ptr(v)=0x51c2fa     ptr(StringBytes(v)=0x51c2fa     ptr([]byte(v)=0xc042021c58
//3 s=hello ptr(v)=0x51c2fa     ptr(StringBytes(v)=0x51c2fa     ptr([]byte(v)=0xc042021c58
//4 s=      ptr(v)=<nil>        ptr(StringBytes(v)=<nil>        ptr([]byte(v)=0xc042021c58
//5 s=      ptr(v)=<nil>        ptr(StringBytes(v)=<nil>        ptr([]byte(v)=0xc042021c58
//6 s=xello ptr(v)=0xc0420444b5 ptr(StringBytes(v)=0xc0420444b5 ptr([]byte(v)=0xc042021c58
//7 s=xello ptr(v)=0xc0420444ba ptr(StringBytes(v)=0xc0420444ba ptr([]byte(v)=0xc042021c58
//Bytes to string:
//0 s=      ptr(v)=0x5c38b8     ptr(StringBytes(v)=0x5c38b8     ptr(string(v)=<nil>
//1 s=hello ptr(v)=0xc0420445e0 ptr(StringBytes(v)=0xc0420445e0 ptr(string(v)=0xc042021c38
//Benchmark_Normal-4    1000000000           0.87 ns/op
//Benchmark_Direct-4    2000000000           0.24 ns/op

结论如下:

1、string常量会在编译期分配到只读段,对应数据地址不可写入,并且相同的string常量不会重复存储。

2、fmt.Sprintf生成的字符串分配在堆上,对应数据地址可修改。

3、常量空字符串有数据地址,动态生成的字符串没有设置数据地址

4、Golang string和[]byte转换,会将数据复制到堆上,返回数据指向复制的数据

5、动态生成的字符串,即使内容一样,数据也是在不同的空间

6、只有动态生成的string,数据可以被黑科技修改

7、string和[]byte通过复制转换,性能损失接近4倍

补充:Golang 使用unsafe.Pointer优化byte[]与String转换性能

我们知道一般来说对于一个String

如果想要转换为byte[]都是通过类型转换语法来实现的:

Res := string(bytes)

这种方式是Go所推荐的,优点就是安全,尽管这种操作会发生内存拷贝,导致性能上会有所损耗,这在处理一般业务时这种损耗是可以忽略的。

但如果是拷贝频繁的情况下,想要进行性能优化时,就需要引入unsafe.Pointer了:

func main()  {
 var s = []byte("我永远喜欢藤原千花.jpg")
 Res := *(*string)(unsafe.Pointer(&s))
 fmt.Println(Res)
}

通过unsafe.Pointer伪造String的过程没有发生内存拷贝,所以效率上会比发生内存拷贝的类型转换快,但代价就是把底层数据暴露出来,这种做法是不安全的。

至于为什么Slice能通过这种方式和String转换

我们可以看下它们的底层结构SliceHeader和StringHeader :

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
  }
type StringHeader struct {
 Data uintptr
 Len  int
  }

两种类型只差了一个字段Cap(容量),前面剩余的字段都是内存对齐的,所以可以直接转换

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

(0)

相关推荐

  • go语言中int和byte转换方式

    主机字节序 主机字节序模式有两种,大端数据模式和小端数据模式,在网络编程中应注意这两者的区别,以保证数据处理的正确性:例如网络的数据是以大端数据模式进行交互,而我们的主机大多数以小端模式处理,如果不转换,数据会混乱 参考 :一般来说,两个主机在网络通信需要经过如下转换过程:主机字节序 -> 网络字节序 -> 主机字节序 大端小端区别 大端模式:Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端 低地址 --------------------> 高地址 高

  • golang中struct和[]byte的相互转换示例

    在网络传输过程中,经常会这样处理:socket接收到数据,先获取其消息头,然后再做各种不同的业务处理.在解析消息头的时候的方法有多种多样.其中最为高效解析消息头的方法就是直接把数据头部分强制类型转换为对应的消息头结构体.这种做法在C/C++中非常的常见.而golang其实也是可以这样子做的.类似这样的应用,直接类型转换获取消息对应的解析方法其实效率会相对较高. golang中struct和[]byte的转换方法,其实就是用到了golang中的unsafe包加上类型转换 , 约束:struct中不

  • 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

  • 简单谈谈Golang中的字符串与字节数组

    前言 字符串是 Go 语言中最常用的基础数据类型之一,虽然字符串往往都被看做是一个整体,但是实际上字符串是一片连续的内存空间,我们也可以将它理解成一个由字符组成的数组,Go 语言中另外一个与字符串关系非常密切的类型就是字节(Byte)了,相信各位读者也都非常了解,这里也就不展开介绍. 我们在这一节中就会详细介绍这两种基本类型的实现原理以及它们的转换关系,但是这里还是会将介绍的重点主要放在字符串上,因为这是我们接触最多的一种基本类型并且后者就是一个简单的 uint8 类型,所以会给予 string

  • golang 的string与[]byte转换方式

    相对于C语言,golang是类型安全的语言.但是安全的代价就是性能的妥协. 下面我们看看Golang不想让我们看到的"秘密"--string的底层数据. 通过reflect包,我们可以知道,在Golang底层,string和slice其实都是struct: type SliceHeader struct { Data uintptr Len int Cap int } type StringHeader struct { Data uintptr Len int } 其中Data是一个

  • 详谈C# 图片与byte[]之间以及byte[]与string之间的转换

    实例如下: //主要通过Stream作为中间桥梁 public static Image ByteArrayToImage(byte[] iamgebytes) { MemoryStream ms = new MemoryStream(iamgebytes); Image image = Image.FromStream(ms); return image; } public static byte[] ImageToByteArray(Image image) { MemoryStream m

  • C#中string与byte[]的转换帮助类-.NET教程,C#语言

    主要实现了以下的函数 代码中出现的sidle是我的网名. /**//*  * @author wuerping  * @version 1.0  * @date 2004/11/30  * @description:  */  using system;  using system.text;  namespace sidlehelper  {  /**//// <summary>  /// summary description for strhelper.  /// 命名缩写:  /// 

  • C# char[]与string byte[]与string之间的转换详解

    1.char[]与string之间的转换 //string 转换成 Char[] string str="hello"; char[] arr=str.ToCharArray(); //Char[] 转换成 string string str1 = new string(arr); 2.byte[]与string之间的转化 string str = "你好,hello"; byte[] bytes; //byte[] 转换成 string bytes = Encod

  • golang 中string和int类型相互转换

    总结了golang中字符串和各种int类型之间的相互转换方式: string转成int: int, err := strconv.Atoi(string) string转成int64: int64, err := strconv.ParseInt(string, 10, 64) int转成string: string := strconv.Itoa(int) int64转成string: string := strconv.FormatInt(int64,10) 字符串到float32/floa

  • Java 大小写最快转换方式实例代码

    Java 大小写最快转换方式实例代码          这里直接给出实现代码,在代码中注释都很清楚,不多做介绍. Java代码 package io.mycat; import java.util.stream.IntStream; /** * 小写字母的 'a'=97 大写字母 A=65 更好相差32利用这个差进行大小写转换 * @author : Hpgary * @date : 2017年5月3日 10:26:26 * @mail: hpgary@qq.com * */ public cl

  • Go中string与[]byte高效互转的方法实例

    目录 前言 数据结构 常规实现 string转[]byte []byte转string 高效实现 性能测试 总结 前言 当我们使用go进行数据序列化或反序列化操作时,可能经常涉及到字符串和字节数组的转换.例如: if str, err := json.Marshal(from); err != nil { panic(err) } else { return string(str) } json序列化后为[]byte类型,需要将其转换为字符串类型.当数据量小时,类型间转换的开销可以忽略不计,但当

  • C#中Byte转换相关的函数

     1.将一个对象转换为byte对象 public static byte GetByte(object o) { byte retInt = 0; if (o != null) { byte tmp; if (byte.TryParse(o.ToString().Trim(), out tmp)) { retInt = tmp; } } return retInt; } 2.将一个十六进制字符串转换为byte对象,字符串以0x开头 public static byte GetByteFormHe

  • C++学习小结之数据类型及转换方式

    一.输入输出语句 Console.ReadLine(); 会等待直到用户按下回车,一次读入一行 Console.ReadKey(); 则是等待用户按下任意键,一次读入一个字符. 二.数据类型 主要掌握: 1.值类型:int 整型,float 浮点型(单精度), double 双精度,char 字符型,bool 布尔型(两种状态true与false),datetime  日期时间 2.引用类型:string 字符串类型 问题? a.字符串与字符的区别:string类型使用 "",char

随机推荐