go的零值使用及注意问题

CKeenGolanggo语言基础约 1578 字大约 5 分钟

作者:程序员CKeen
博客:http://ckeen.cnopen in new window

长期坚持做有价值的事!积累沉淀,持续成长,升维思考!希望把编码作为长期兴趣爱好😄


1. 背景

今天在集成阿里云物联网平台的服务端接口的go SDK的时候,调用了一个阿里云物联网平台查询设备详情的接口,代码如下


func (deviceService *DeviceService) GetDeviceDetail(config pub.ProductConfig, imei string) (*model.DeviceDetail, error) {
	client, err := client2.GetInstance().WithClient(config.AccessKeyId, config.AccessKeySecret, config.RegionId)
	if err != nil {
		return &model.DeviceDetail{}, err
	}
	queryDetailReq := &iot.QueryDeviceDetailRequest{
		IotInstanceId: &config.IotInstanceId,
		ProductKey:    &config.ProductKey,
		DeviceName:    &imei,
		IotId:         nil,
	}

	resp, err := client.QueryDeviceDetail(queryDetailReq)
	if err != nil {
		return &model.DeviceDetail{} ,err
	}

	if resp.Body.Success != nil && *resp.Body.Success {
		return &model.DeviceDetail{
			Imei:     *resp.Body.Data.DeviceName,
			OutIotId: *resp.Body.Data.IotId,
			Online:   (strings.ToLower(*resp.Body.Data.Status)) == "online",
		},nil
	}

	return nil ,RequestFailError

}

代码中可以看到阿里云物联网go-sdk封装的所有的请求参数和返回参数都是使用指针类型的。于是我好奇的看下QueryDeviceDetailResponse源码定义:

type QueryDeviceDetailResponse struct {
	Headers map[string]*string             `json:"headers,omitempty" xml:"headers,omitempty" require:"true"`
	Body    *QueryDeviceDetailResponseBody `json:"body,omitempty" xml:"body,omitempty" require:"true"`
}

type QueryDeviceDetailResponseBody struct {
	RequestId    *string                            `json:"RequestId,omitempty" xml:"RequestId,omitempty"`
	Success      *bool                              `json:"Success,omitempty" xml:"Success,omitempty"`
	Code         *string                            `json:"Code,omitempty" xml:"Code,omitempty"`
	ErrorMessage *string                            `json:"ErrorMessage,omitempty" xml:"ErrorMessage,omitempty"`
	Data         *QueryDeviceDetailResponseBodyData `json:"Data,omitempty" xml:"Data,omitempty" type:"Struct"`
}

type QueryDeviceDetailResponseBodyData struct {
	IotId           *string `json:"IotId,omitempty" xml:"IotId,omitempty"`
	ProductKey      *string `json:"ProductKey,omitempty" xml:"ProductKey,omitempty"`
	ProductName     *string `json:"ProductName,omitempty" xml:"ProductName,omitempty"`
	DeviceName      *string `json:"DeviceName,omitempty" xml:"DeviceName,omitempty"`
	DeviceSecret    *string `json:"DeviceSecret,omitempty" xml:"DeviceSecret,omitempty"`
	FirmwareVersion *string `json:"FirmwareVersion,omitempty" xml:"FirmwareVersion,omitempty"`
	GmtCreate       *string `json:"GmtCreate,omitempty" xml:"GmtCreate,omitempty"`
	UtcCreate       *string `json:"UtcCreate,omitempty" xml:"UtcCreate,omitempty"`
	GmtActive       *string `json:"GmtActive,omitempty" xml:"GmtActive,omitempty"`
	UtcActive       *string `json:"UtcActive,omitempty" xml:"UtcActive,omitempty"`
	GmtOnline       *string `json:"GmtOnline,omitempty" xml:"GmtOnline,omitempty"`
	UtcOnline       *string `json:"UtcOnline,omitempty" xml:"UtcOnline,omitempty"`
	Status          *string `json:"Status,omitempty" xml:"Status,omitempty"`
	IpAddress       *string `json:"IpAddress,omitempty" xml:"IpAddress,omitempty"`
	NodeType        *int32  `json:"NodeType,omitempty" xml:"NodeType,omitempty"`
	Region          *string `json:"Region,omitempty" xml:"Region,omitempty"`
	Owner           *bool   `json:"Owner,omitempty" xml:"Owner,omitempty"`
	Nickname        *string `json:"Nickname,omitempty" xml:"Nickname,omitempty"`
}

所有使用的都是指针类型,再看一下其他的接口,全是使用的指针类型的定义。一般我们想到的都是直接定义个基础的类型,这里阿里go sdk为什么要这么做呢?

于是我花了一些时间查询到一些资料,发现这个应该是为了区分go的**变量的零值问题**。

2. go语言中的零值

我们知道,当通过声明或 new 调用为变量分配存储空间时,或通过复合文字或 make 调用创建新值时,且未提供显式初始化,则给出变量或值一个默认值

变量或值的每个元素都为其类型设置为零值:布尔型为 false,数字类型为 0,字符串为 "",指针、函数、接口、切片、通道和map为 nil。此初始化是递归完成的,例如,如果未指定任何值,则结构体数组的每个元素的字段都将其清零。下面展示了一些类型的零值:

	var i int
	var b bool
	var s string
	fmt.Printf("i=%v, b=%v, s=%s\n",i,b ,s)
    // 打印结果:i=0, b=false, s=

	var pointer *string
    var f func(string)
	var ii interface{}
	var c chan struct{}
	var slice1 []string
	var map1 map[string]string

	fmt.Printf("pointer=%v, func=%v, interface=%v, chan=%v,slice=%v,  map=%v\n",
		pointer, f , ii , c, slice1, map1)
	fmt.Printf("pointer=%v, func=%v, interface=%v, chan=%v,slice=%v,  map=%v\n",
		pointer == nil, f == nil , ii == nil, c == nil, slice1 == nil, map1 == nil)

    // 打印结果:pointer=<nil>, func=<nil>, interface=<nil>, chan=<nil>,slice=[],  map=map[]
    // 打印结果:pointer=true, func=true, interface=true, chan=true, slice=true,  map=true

我们可以看到,通过零值判断进行默认值赋值,这样可以减少一些数据未初始化的错误,增强了Go程序的健壮性。 但是,同时也引入了一些问题,比如

  • 未显示初始化的切片、map,他们可以直接操作,但是不能写入数据,否则会引发程序panic
  • 零值的指针就是指向nil的指针,无法直接进行运算,因为是没有无内容的地址
  • error内置接口类型是表示错误条件的常规接口,nil值表示没有错误,所以调用Error方法时类型error不能是零值,否则会引发panic
  • ``在日常开发中我们会使用到闭包,但是这其中隐藏一个问题,如果我们函数忘记初始化了,那么就会引发panic

等等一些操作不当可能引发的panic问题,所以在使用的零值也需要注意。

3. 区分零值和赋值的关系

我们现在知道了go的零值及其存在的一些问题,那上述说的阿里go sdk都定义为指针类型和零值又有什么关系呢?

下面我们来详细分析一下:

假如我们不用指针定义,直接使用基础的string,bool,int等类型定义,在结构初始化的时候这些基本上类型都有初始化的零值。然后我们通过阿里的接口访问的服务器返回相应的数据,对结构的数据进行填充。 如果我们取结构中的字段为零值,怎么判断是初始化的默认值,还是访问通过结果返回的零值呢?所以这里是没办法判断。

所以这里定义返回的数据都是指针类型,如果是初始化的零值,那么指针类型的零值为nil;如果是通过请求服务器返回的,那么一定会使用返回的值填充结构字段数据,这个时候指针类型也不为空。最终我们可以通过判断指针类型的数据是否为nil,来判断是否有返回的数据填充相应的字段,如果指针类型的数据不为nil,再去取相应字段的实际值。所以我们在判断请求是否成功的时候,进行的如下操作:

	if resp.Body.Success != nil && *resp.Body.Success {
		return &model.DeviceDetail{
			Imei:     *resp.Body.Data.DeviceName,
			OutIotId: *resp.Body.Data.IotId,
			Online:   (strings.ToLower(*resp.Body.Data.Status)) == "online",
		},nil
	}

先判断Sucess的指针不为nil,再取实际的Success指针的值进行判断。

所以使用指针类型定义结构字段是为了区分字段的零值和请求返回对字段的是否填充值。