关于go:在不同类型的切片之间转换

Convert between slices of different types

我从一个udp套接字中得到一个字节片([]byte),并希望在不更改底层数组的情况下将其视为一个整数片([]int32),反之亦然。在C(++)中,我只需要在指针类型之间进行强制转换;如何在Go中执行此操作?


正如其他人所说,在go中投射指针被认为是不好的形式。下面是正确的行距和C数组转换的等价物的例子。

警告:所有代码未测试。

正确的方法

在本例中,我们使用encoding/binary包将每组4个字节转换为int32。这更好,因为我们正在指定endianness。我们也没有使用unsafe包来破坏类型系统。

1
2
3
4
5
6
7
8
9
import"encoding/binary"

const SIZEOF_INT32 = 4 // bytes

data := make([]int32, len(raw)/SIZEOF_INT32)
for i := range data {
    // assuming little endian
    data[i] = int32(binary.LittleEndian.Uint32(raw[i*SIZEOF_INT32:(i+1)*SIZEOF_INT32]))
}

号错误的方式(C数组转换)

在这个例子中,我们告诉Go忽略类型系统。这不是一个好主意,因为它可能在Go的另一个实现中失败。它假设语言规范中没有这些内容。但是,这一个不做完整的拷贝。此代码使用unsafe访问"sliceheader",这在所有切片中都很常见。头包含指向数据(C数组)、长度和容量的指针。我们首先需要更改长度和容量,而不是将头转换为新的切片类型,因为如果我们将字节视为新类型,那么元素就更少了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import (
   "reflect"
   "unsafe"
)

const SIZEOF_INT32 = 4 // bytes

// Get the slice header
header := *(*reflect.SliceHeader)(unsafe.Pointer(&raw))

// The length and capacity of the slice are different.
header.Len /= SIZEOF_INT32
header.Cap /= SIZEOF_INT32

// Convert slice header to an []int32
data := *(*[]int32)(unsafe.Pointer(&header))


简短的回答是你不能。Go不会让你把一种类型的切片投射到另一种类型的切片上。您将在数组中循环,并在强制转换数组中的每个项时创建另一个所需类型的数组。这通常被认为是一件好事,因为字体安全是go的一个重要特性。


您在C中所做的操作,有一个例外——go不允许从一个指针类型转换为另一个指针类型。是的,但你必须使用不安全的指针来告诉编译器你知道所有的规则都被破坏了,你知道你在做什么。下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
   "fmt"
   "unsafe"
)

func main() {
    b := []byte{1, 0, 0, 0, 2, 0, 0, 0}

    // step by step
    pb := &b[0]         // to pointer to the first byte of b
    up := unsafe.Pointer(pb)    // to *special* unsafe.Pointer, it can be converted to any pointer
    pi := (*[2]uint32)(up)      // to pointer to the first uint32 of array of 2 uint32s
    i := (*pi)[:]           // creates slice to our array of 2 uint32s (optional step)
    fmt.Printf("b=%v i=%v
", b, i)

    // all in one go
    p := (*[2]uint32)(unsafe.Pointer(&b[0]))
    fmt.Printf("b=%v p=%v
", b, p)
}

显然,您应该小心使用"不安全"的包,因为Go编译器不再握着您的手-例如,您可以在这里编写pi := (*[3]uint32)(up),编译器不会抱怨,但您会遇到麻烦。

另外,正如其他人已经指出的,uint32的字节在不同的计算机上的布局可能不同,所以您不应该假设这些是您需要的布局。

所以最安全的方法是逐个读取字节数组,并从中提取所需的内容。

亚历克斯


我遇到了大小未知的问题,并用下面的代码修改了前面的不安全方法。给定字节片B…

1
int32 slice is (*(*[]int)(Pointer(&b)))[:len(b)/4]

对于要切片的数组示例,可以给出一个虚构的大常量,并且切片边界的使用方式相同,因为没有分配数组。


也许在给出早期的答案时,它是不可用的,但似乎binary.Read方法比上面给出的"正确的方法"更好。

此方法允许您将二进制数据从读卡器直接读取到所需类型的值或缓冲区中。您可以通过在字节数组缓冲区上创建一个读卡器来实现这一点。或者,如果您拥有为您提供字节数组的代码的控制权,那么您可以替换它直接读取缓冲区,而不需要临时字节数组。

请参阅https://golang.org/pkg/encoding/binary/阅读文档和一个很好的小示例。


你可以用"不安全"的包裹

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package main

import (
   "fmt"
   "unsafe"
)

func main() {
    var b [8]byte = [8]byte{1, 2, 3, 4, 5, 6, 7, 8}
    var s *[4]uint16 = (*[4]uint16)(unsafe.Pointer(&b))
    var i *[2]uint32 = (*[2]uint32)(unsafe.Pointer(&b))
    var l *uint64 = (*uint64)(unsafe.Pointer(&b))

    fmt.Println(b)
    fmt.Printf("%04x, %04x, %04x, %04x
", s[0], s[1], s[2], s[3])
    fmt.Printf("%08x, %08x
", i[0], i[1])
    fmt.Printf("%016x
", *l)
}

/*
 * example run:
 * $ go run /tmp/test.go
 * [1 2 3 4 5 6 7 8]
 * 0201, 0403, 0605, 0807
 * 04030201, 08070605
 * 0807060504030201
 */


网址:http://play.golang.org/p/wam5cs-ecz

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
   "fmt"
   "strings"
)

func main() {
    s := []interface{}{"foo","bar","baz"}
    b := make([]string, len(s))
    for i, v := range s {
        b[i] = v.(string)
    }
    fmt.Println(strings.Join(b,","))
}