[译] 探索 Go 中字节解析 API
很多 年前,我开始研究 Linux
的 Netlink
进程间通信接口。 N==etlink
被用于从 Linux
内核检索信息,并且为了跨越内核边界,信息通常被打包到 Netlink
的属性中。 经过一些实验,我为 Go
创建了自己的 netlink
包。
随着时间的推移,包中的 API
已经有很大的改变了。特别是 Netlink
属性总是处理起来相当复杂。今天,我们将探索一些我为处理 Netlink
属性所创建的字节解析 API
。这里描述的技术应该也能广泛应用于许多其他的 Go
库和应用程序中!
Netlink 属性简介
Netlink
属性被以类型 / 长度 / 值或 TLV
格式打包,与许多二进制网络协议情况一样。这种格式具有很好的扩展性,因为许多属性可以在单个字节切片中被打包成自发自收的形式。
属性中的值可以包含:
-
无符号 8/16/32/64 位整形
-
以 null 结尾的 C 字符串
-
任意 C 结构字节
-
嵌套 Netlink 属性
-
Netlink 属性数组
为了我们的目标,我们可以在 Go
中像下面这样定义一个 Netlink
属性:
1 type Attribute struct { 2 // The type of this Attribute, typically matched to a constant. 3 Type uint16 4 5 // Length omitted; Data will be a byte slice of the appropriate length. 6 7 // An arbitrary payload which is specified by Type. 8 Data []byte 9 }
今天,我们将略过低级的字节解析逻辑而有利于讨论各种高级 API
,但是你能从 我关于 Netlink
系列博客 中学习到更多的关于处理 Netlink
属性的知识。
字节解析 API 第一版
单个字节切片可以包含许多 Netlink
属性。让我们来定义一个初始的解析函数,它接收一个字节切片的输入并且返回一个属性切片。
1 // UnmarshalAttributes unpacks a slice of Attributes from a single byte slice. 2 func UnmarshalAttributes(b []byte) ([]Attribute, error) { 3 // ... 4 }
举个例子,假设我们想从属性切片中解包一个 uint16
和 string
值。你可以放心地忽略 parseUint16
和 parseString
;它们将处理一些 Netlink 属性数据中棘手的部分。
为了解包属性数据,我们可以在 Type
属性上使用循环和匹配:
1 attrs, err := netlink.UnmarshalAttributes(b) 2 if err != nil { 3 return err 4 } 5 6 var ( 7 num uint16 8 str string 9 ) 10 11 for _, a := range attrs { 12 switch a.Type { 13 case 1: 14 num = parseUint16(a.Data[0:2]) 15 case 2: 16 str = parseString(a.Data) 17 } 18 } 19 20 fmt.Printf("num: %d, str: %q", num, str) 21 // num: 1, str: "hello world"
这样可以正常工作,但是有一个问题:如果我们 uint16
值的字节切片比 2
字节多或者少,会出现什么情况呢?
1 // A panic waiting to happen! 2 num = parseUint16(a.Data[0:2])
如果它少于 2
字节,此代码将出现 panic
,并且让你的应用程序挂掉。如果它超过 2
字节,我们就默默地忽略任何额外的数据(这个值实际上不是 uint16 !
)。
添加验证和错误处理
我们稍微修改一下我们的解析函数。每一个都应该做一些内部验证,如果字节切片不满足我们的限制,我们可以返回一个 error
。
1 attrs, err := netlink.UnmarshalAttributes(b) 2 if err != nil { 3 return err 4 } 5 6 var ( 7 num uint16 8 str string 9 10 // Used to check for errors without shadowing num and str later. 11 err error 12 ) 13 14 for _, a := range attrs { 15 // This works, but it's a bit verbose. 16 // Be cautious of variable shadowing as well! 17 switch a.Type { 18 case 1: 19 num, err = parseUint16(a.Data) 20 case 2: 21 str, err = parseString(a.Data) 22 } 23 if err != nil { 24 return err 25 } 26 } 27 28 fmt.Printf("num: %d, str: %q", num, str) 29 // num: 1, str: "hello world"
这样也是有效的,但你必须对你的错误检查策略保持谨慎,并且确保你不会意外地使用 :=
赋值运算符屏蔽掉你尝试解包的其中一个变量。
我们可以进一步改进这种模式吗?
一个类似迭代器的解析 API
上述的策略正常运行了许多年,但在编写了一些 Netlink
交互包之后,我决定开始改进 API
。
新的 API
使用类似迭代器的模式,其灵感来自于标准库中的 bufio.Scanner API。Go
的博客 Errors are values
这篇文章同样为解释这个策略做了出色的工作。
netlink.AttributeDecoder
类型就是一个类似迭代器的解析 API
。在使用了 netlink.NewAttributeDecoder
构造器之后,许多方法被暴露出来,其能够与内部属性切片进行交互:
-
Next:将内部指针指向下一个属性
-
Type:返回当前属性的类型值
-
Err:返回在迭代期间遇到的第一个错误
在尝试这个新的 API 时,让我们重温前面的例子:
1 ad, err := netlink.NewAttributeDecoder(b) 2 if err != nil { 3 return err 4 } 5 6 var ( 7 num uint16 8 str string 9 ) 10 11 // Continue advancing the internal pointer until done or error. 12 for ad.Next() { 13 // Check the current attribute's type and extract it as appropriate. 14 switch ad.Type() { 15 case 1: 16 // If data isn't a uint16, an error will be captured internally. 17 num = ad.Uint16() 18 case 2: 19 str = ad.String() 20 } 21 } 22 23 // Check for the first error encountered during iteration. 24 if err := ad.Err(); err != nil { 25 return err 26 } 27 28 fmt.Printf("num: %d, str: %q", num, str) 29 // num: 1, str: "hello world"
有很多种方法可以用于迭代器期间提取的数据,例如 Uint8/16/32/64
、 Bytes
、 string
和所有的最有用的方法,包括: Do
。
Do 是一种特殊用途的方法,允许解码器处理任意数据,如 C
结构、嵌套的 Netlink
属性、 Netlink
数组。它能接受一个闭包,并将解码器所指向的当前数据传递给闭包。
为了处理嵌套 Netlink
属性,创建另外的包含一个 Do
闭包的 AttributrEncoder
:
1 ad.Do(func(b []byte) error) { 2 nad, err := netlink.NewAttributeDecoder(b) 3 if err != nil { 4 return err 5 } 6 7 if err := handleNested(nad); err != nil { 8 return err 9 } 10 11 // Make sure to propagate internal errors to the top-level decoder! 12 return nad.Err() 13 })
为了保持小的闭包体,可以定义辅助函数来解析 Netlink 属性中的任意类型:
1 // parseFoo returns a function compatible with Do. 2 func parseFoo(f *Foo) func(b []byte) error { 3 return func(b []byte) error { 4 // Some parsing logic... 5 foo, err := unpackFoo(b) 6 if err != nil { 7 return err 8 } 9 10 // Store foo in f by dereferencing the pointer. 11 *f = foo 12 return nil 13 } 14 }
现在,这个辅助函数可以直接用于 Do:
1 var f Foo 2 ad.Do(parseFoo(&f))
此 API
为它的调用者提供了极大的灵活性。所有的错误传播都在内部处理,并通过从顶级解码器调用 Err
方法将错误冒泡到调用者。
结论
虽然花了一些时间和实验,但是我对 netlink.AttributeDecoder
中类似迭代器字节解析 API
感到非常满意。它非常适合于我的需求,感谢 Terin Stock
,我们还添加了一个 对称编码器 API
,其灵感来自于解码器 API
的成功!
如果你正在开发一个你并不满意的包 API
,标准库是寻找灵感的好地方!我也强烈建议与各种 Go
帮助社区 取得联系,因为有很多人非常愿意提供出色的建议和批评!
如果你有任何问题,请随时和我联系!我在 Gophers Slack
、 Github
和 Twitter
的称号是 mdlayher
。
链接
-
netlink
包 -
Linux、Netlink
和Go
博客系列 -
Go
博客:Errors are values
-
bufio.Scanner
-
netlink.AttributeDecoder
via: https://blog.gopheracademy.com/advent-2018/exploring-byte-parsing-apis-in-go/
作者:Matt Layher
译者:PotoYang
校对:polaris1119
本文由 GCTT 原创编译,Go语言中文网 荣誉推出