Go位移操作

背景

今天有一个需求,是我需要拼接一个2字节的byte,且要根据要求按位拼接,这时候就需要我们用到位移操作。

Coding Now

位移操作很容易理解了,2字节我们先设置一个uint16类型的来存我们最终的值,这里我们假定两种情况,

  • 情况一:16位都没有关系需要逐位拼接

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    package main
    import(
    "fmt"
    "encoding/binary"
    "github.com/imroc/biu"
    )
    func main() {
    var m uint16 = 0
    m = m | 1
    m = m << 1
    m = m | 0
    m = m << 1
    .... // 省略同上
    m = m | 1
    m = m << 1
    m = m | 0
    //m = m << 1
    s := make([]byte, 2)
    binary.BigEndian.PutUint16(s, m)
    fmt.Println(biu.BytesToBinaryString(s)) // 这个函数是引用了github中的一个库,可以将[]byte转为二进制字符串
    }

    输出如下

    WechatIMG43

  • 情况二:我们假定将16位分成前6位和后10位

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    package main
    import(
    "fmt"
    "encoding/binary"
    "github.com/imroc/biu"
    )
    func main() {
    var x uint16 = 512 // 后10位
    var m uint16 = 0 // 初始化
    var l uint16 = 45 // 前6位
    m = m | l // 先与前6位取或
    m = m << 10 // 然后左移10位
    m = m | x // 与后10位取或
    s := make([]byte, 2)
    binary.BigEndian.PutUint16(s, m) // 用大端的方式将uint16的m放入我们新建的[]byte中
    fmt.Println(biu.BytesToBinaryString(s))
    }

    输出如下:

    WechatIMG44

  • 然后Golang的左移有一个坑点,左移如果溢出的话不会截断而是会报错,我们看下边这段代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    package main
    import(
    "fmt"
    )
    func main() {
    const m uint8 = 255
    fmt.Println(m << 4)
    }

    我们会发现报错了,错误如下:

    WechatIMG45

    左移溢出的时候,go默认会报错而不是截断,如果把m变成变量,我们就会发现它不报错了。

    为什么变量没有报溢出的错误呢?因为它是变量,编译器是在运行期才会给变量分配存储内存的。

    而编译器会保证常量不变化,常量的赋值是一个编译期行为,所以常量通常会被编译器在预处理阶段直接展开,于是它发现你给uint8赋了一个超过它可存储最大值的数,报错是必然的结果。

    同理给常量赋一个编译期不可知的值也是不行的,比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    package main
    import (
    "fmt"
    )
    func add(x, y int) uint16 {
    return uint16(x + y)
    }
    func main() {
    const m uint16 = add(3, 4)
    fmt.Println(m)
    }

    add(x, y int)只有在运行期才能知道返回结果,所以也会报错。

    WechatIMG46

    从官方文档中对于常量的描述我们也可以看出,But in Go, a constant is just a simple, unchanging value,在Go的设计中,常量只是一个简单的不可变的值,所以这样的设定大概只是为了强制你避免溢出或者超范围的情况。

  • 所以,你可能需要先用不定长度的uint类型绕开溢出的限制,再用掩码(& 0xFFFF)运算去约束结果的范围,(如果是uint8就用0xFF),加了判断溢出的处理之后,代码是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    package main
    import(
    "fmt"
    "encoding/binary"
    "github.com/imroc/biu"
    )
    func main() {
    var size uint16 = 512
    var m uint16 = 0
    var l uint16 = 45
    m = m | l
    m = uint16(uint(m) << 10 & 0xFFFF)
    //m = m << 10
    m = m | size
    s := make([]byte, 2)
    binary.BigEndian.PutUint16(s, m)
    fmt.Println(biu.BytesToBinaryString(s))
    }

总结

Go中有一些设定很奇怪,有时候报错很难找到症结,比如binary中的方法已经设定了[]byte的字节数,如果不是固定字节数就一定会报错这种事情,也是很狗血,但这不妨碍我爱Go

正是因为Go的的这些狗血的设定,所以避免了很多奇怪的错误,正如官方文档所说,C/C++的很多奇怪的bug、错误、野指针等等的问题,就是因为这些不同类型的混用而导致的,因此在Go最初的设置中才会有这些奇怪的要求,这也是为了避免后边出现更难处理的错误。关于Go狗血的要求,比如:没有用到的包必须删掉、可被外部调用的成员或方法首字母必须大写、第一个大括号不能另起一行,go fmt等等,可能也是为了强制代码规范,不得不说,大部分Go代码看上去都让人比较愉悦 :)

就这样,Thx~

参考文档

左移溢出处理

Golang/Constants