什么情况下在 Go 中使用 <基础类型指针>
04 Sep 2019 - Joey
本文将介绍我在写 oVirt Go SDK 时,struct 内属性定义为指针。
前言
读书学 C 时我知道了指针数组和数组指针,使我对指针有了份特殊的感情。后来做项目我用了5年 Java, 近3年用 Python,本以为这辈子都不会碰指针了,但没想到 Go 的横空出世,让我和指针又续上了缘分。
我还是 Go 菜鸟,写代码没过 3k 行。于是对指针的理解上就比较狭隘:指针,基本是用来修饰 struct 的,而基础类型(string/bool/int 等)的指针,只有一个用处:作为函数的参数,将传值改为传引用,在函数内部修改传入的参数值。
直到后来我在做 oVirt Go SDK 时,oVirt 社区的 maintainers 给我提了一些修改建议,其中就使用基础类型指针,很巧妙的解决了一个棘手问题。
下面先介绍基础类型指针比较基础的一个应用:函数参数,然后介绍它的另一大用处。
基础类型指针作为函数参数
这个作用比较基础,因为 Go 的函数参数都是传值,所以如果想在函数内部修改参数值,就必须传指针。比如下面这个例子:执行代码
package main
import (
"fmt"
)
func CannotModify(value int64) {
value = 63
}
func CanModify(value *int64) {
*value = 500
}
func main() {
var value int64 = 65
CannotModify(value)
fmt.Printf("value is %v, without modified\n", value)
CanModify(&value)
fmt.Printf("value is %v, with modified\n", value)
}
根据结果可以看到,函数 CanModify 使用 *int64 作为参数,成功修改了变量 value 的值。
Unmarshal 时判断是否有值
oVirt SDK 是通过调用 oVirt 的接口实现管理功能,接口的数据都是 XML 格式。所以在实现时少不了使用 Go 的 xml.Marshall 和 xml.Unmarshall 函数实现 XML 与 Go struct 之间的转换。
写到这里,需要先介绍一下 Go 的零值,其实在 Java 和 Python 中,任何类型(包含基础类型)的变量,在赋值都可以设置为 null/None。但 Go 下,基础类型的零值并不是 nil,也不能设为 nil。如:string 的零值是 ““,bool 的零值是 false,int 的零值是0。
以下面这段代码介绍 Go 零值的一个问题:(执行代码)
package main
import (
"fmt"
"encoding/xml"
)
type VM struct {
Name string `xml:"name,omitempty"`
Desc string `xml:"desc,omitempty"`
}
type VMPtr struct {
Name *string `xml:"name,omitempty"`
Desc *string `xml:"desc,omitempty"`
}
var xmlStr = `
<vm>
<name>vm-name</name>
</vm>
`
var xmlStr2 = `
<vm>
<name>vm-name</name>
<desc></desc>
</vm>
`
func main() {
var vm1 VM
xml.Unmarshal([]byte(xmlStr), &vm1)
fmt.Printf("vm1 name is %#v, desc is %#v\n", vm1.Name, vm1.Desc)
var vm2 VM
xml.Unmarshal([]byte(xmlStr2), &vm2)
fmt.Printf("vm2 name is %#v, desc is %#v\n", vm2.Name, vm2.Desc)
// 对于以上代码,如果 Desc 属性是 string 类型,则在 Unmarshal 时无法区分 xml 中:<不存在desc属性> 还是 <desc是空串("")>
var vmPtr1 VMPtr
xml.Unmarshal([]byte(xmlStr), &vmPtr1)
fmt.Printf("vm (ptr) name is %#v(addr:%v), desc is %#v(addr: not exits)\n", *vmPtr1.Name, vmPtr1.Name, vmPtr1.Desc)
var vmPtr2 VMPtr
xml.Unmarshal([]byte(xmlStr2), &vmPtr2)
fmt.Printf("vm (ptr) name is %#v(addr:%v), desc is %#v(addr:%v)\n", *vmPtr2.Name, vmPtr2.Name, *vmPtr2.Desc, vmPtr2.Desc)
// 而如果 Desc 属性是 *string 类型,则就可以通过 <*string 指针是否是 nil> 去判断xml中是否有desc属性了
}
从以上代码的注释和输出,可以看出来使用 *string 可以区分出 VM 的 Desc 属性是否在 xml.Unmarshal 过程中被赋值了。而这,就是基础类型指针的最大用处了。
附加福利:如果使用基础类型指针(比如*string),在 xml.Marshal 时,不用在 tag 中定义omitempty,xml.Marshal 自动不输出 nil 的指针。以下面代码为例:执行代码
package main
import (
"fmt"
"encoding/xml"
)
type VM struct {
Name string `xml:"name,omitempty"`
Desc string `xml:"desc"`
}
type VMPtr struct {
Name *string `xml:"name"`
Desc *string `xml:"desc"`
}
func main() {
var vm VM
vm.Name = "vm-name"
xmlVMBytes, _ := xml.Marshal(vm)
fmt.Printf("vm marshal is %v\n", string(xmlVMBytes))
fmt.Println("----------------------------------------")
var vmPtr VMPtr
temp := "vmPtr-name"
vmPtr.Name = &temp
xmlVMPtrBytes, _ := xml.Marshal(vmPtr)
fmt.Printf("vmPtr marshal is %v\n", string(xmlVMPtrBytes))
}
总结
基础类型指针在 xml/json/ini 的 Unmarshal 过程中,可以自动判断属性是否被赋值,同时在 Marshal 过程中,可以不用在 tag 中定义 omitempty。