go语言定义“零值可用”的类型

九路 等级 506 1 0

1. Go 类型的零值

作为 C 程序员出身的我,我总是喜欢用在使用 C 语言的”受过的苦“与 Go 语言中得到的”甜头“做比较,从而来证明 Go 语言设计者在当初设计 Go 语言时是做了充分考量的。

在 C99 规范中,有一段是否对栈上局部变量进行自动清零初始化的描述:

如果未显式初始化且具有自动存储持续时间的对象,则其值是不确定的。

规范的用语总是晦涩难懂的。这句话大致的意思就是:如果是在栈上分配的局部变量,且在声明时未对其进行显式初始化,那么这个变量的值是不确定的。比如:

// varinit.c
#include <stdio.h>

static int cnt;

void f() {
    int n;
    printf("local n = %d\n", n);

    if (cnt > 5) {
        return;
    }

    cnt++;
    f();
}

int main() {
    f();
    return 0;
}

编译上面的程序并执行:

// 环境 centos linux gcc 版本 4.1.2
// 注意:在您的环境中执行上述代码,输出的结果很大可能与这里有所不同
$ gcc varinit.c
$ ./a.out

local n = 0
local n = 10973
local n = 0
local n = 52
local n = 0
local n = 52
local n = 52

我们看到分配在栈上的未初始化变量的值是不确定的,虽然一些编译器的较新版本也都提供一些命令行参数选项用于对栈上变量进行零值初始化,比如 GCC 就提供如下命令行选项:

-finit-local-zero
-finit-derived
-finit-integer=n
-finit-real=<zero|inf|-inf|nan|snan>
-finit-logical=<true|false>
-finit-character=n

但这并不能改变 C 语言原生不支持对未显式初始化局部变量进行零值初始化的事实。资深 C 程序员是深知这个陷阱带来的问题是有多严重的。因此同样出身于 C 语言的 Go 设计者们在 Go 中彻底对这个问题进行的修复和优化。根据Go 语言规范

当通过声明或调用new为变量分配存储空间,或者通过复合文字字面量或make调用创建新值, 并且还不提供显式初始化的情况下,Go会为变量或值提供默认值。

Go 语言的每种原生类型都有其默认值,这个默认值就是这个类型的零值。下面是 Go 规范定义的内置原生类型的默认值(零值)。

所有整型类型:0
浮点类型:0.0
布尔类型:false
字符串类型:""
指针、interface、slice、channel、map、function:nil

另外 Go 的零值初始是递归的,即诸如数组、结构体等类型的零值初始化就是对其组成元素逐一进行零值初始化。

2. 零值可用

我们现在知道了 Go 类型的零值,接下来我们来说“可用”。

Go 从诞生以来就秉承着尽量保持“零值可用”的理念,我们来看两个例子。

第一个例子是关于 slice 的:

var zeroSlice []int
zeroSlice = append(zeroSlice, 1)
fmt.Println(zeroSlice) // 输出:[1]

我们声明了一个 []int 类型的 slice:zeroSlice,我们并没有对其进行显式初始化,这样 zeroSlice 这个变量被 Go 编译器置为零值:nil。按传统的思维,对于值为 nil 这样的变量我们要给其赋上合理的值后才能使用。但是 Go 具备零值可用的特性,我们可以直接对其使用 append 操作,并且不会出现引用 nil 的错误。

第二个例子是通过 nil 指针调用方法的:

// callmethodthroughnilpointer.go
package main

import (
        "fmt"
        "net"
)

func main() {
        var p *net.TCPAddr
        fmt.Println(p) //输出:<nil>
}

我们声明了一个 net.TCPAddr 的指针变量,我们并未对其显式初始化,指针变量 p 会被 Go 编译器赋值为 nil。我们在标准输出上输出该变量,fmt.Println 会调用 p.String()。我们来看看 TCPAddr 这个类型的 String 方法实现:

// $GOROOT/src/net/tcpsock.go
func (a *TCPAddr) String() string {
        if a == nil {
                return "<nil>"
        }
        ip := ipEmptyString(a.IP)
        if a.Zone != "" {
                return JoinHostPort(ip+"%"+a.Zone, itoa(a.Port))
        }
        return JoinHostPort(ip, itoa(a.Port))
}

我们看到 Go 标准库在定义 TCPAddr 类型以及其方法时充分考虑了“零值可用”的理念,使得通过值为 nil 的 TCPAddr 指针变量依然可以调用 String 方法。

在 Go 标准库和运行时代码中还有很多践行“零值可用”理念的好例子,最典型的莫过于 sync.Mutex 和 bytes.Buffer 了。

我们先来看看 sync.Mutex。在 C 语言中,如果我们要使用线程互斥锁,我们需要这么做:

pthread_mutex_t mutex; // 不能直接使用

// 必须先进行初始化
pthread_mutex_init (&mutex, NULL);

// 然后才能执行lock或unlock
pthread_mutex_lock(&mutex); 
pthread_mutex_unlock(&mutex); 

但是在 Go 语言中,我们只需这么做:

var mu sync.Mutex
mu.Lock()
mu.Unlock()

Go 标准库的设计者很“贴心”地将 sync.Mutex 结构体的零值状态设计为可用状态,这样让 Mutex 的调用者可以“省略”对 Mutex 的初始化而直接使用 Mutex。

Go 标准库中的 bytes.Buffer 亦是如此:

// bytesbufferwrite.go
package main

import (
        "bytes"
)

func main() {
        var b bytes.Buffer
        b.Write([]byte("Effective Go"))
        fmt.Println(b.String()) // 输出:Effective Go
}

我们看到我们无需对 bytes.Buffer 类型的变量 b 进行任何显式初始化即可直接通过 b 调用其方法进行写入操作,这源于 bytes.Buffer 底层存储数据的是同样支持零值可用策略的 slice 类型:

// $GOROOT/src/bytes/buffer.go
// A Buffer is a variable-sized buffer of bytes with Read and Write methods.
// The zero value for Buffer is an empty buffer ready to use.
type Buffer struct {
        buf      []byte // contents are the bytes buf[off : len(buf)]
        off      int    // read at &buf[off], write at &buf[len(buf)]
        lastRead readOp // last read operation, so that Unread* can work correctly.
}

3. 小结

Go 语言零值可用的理念给内置类型、标准库的使用者带来很多便利。不过 Go 并非所有类型都是零值可用的,并且零值可用也是有一定限制的,比如:slice 的零值可用不能通过下标形式操作数据:

var s []int
s[0] = 12 // 报错!
s = append(s, 12) // OK

另外像 map 这样的内置类型也没有提供零值可用的支持:

var m map[string]int
m["tonybai"] = 1 // 报错!

m1 := make(map[string]int
m1["tonybai"] = 1 // OK

另外零值可用的类型要注意尽量避免值拷贝:

var mu sync.Mutex
mu1 := mu // Error: 避免值拷贝
foo(mu) // Error: 避免值拷贝

我们可以通过指针方式传递类似 Mutex 这样的类型。

对于我们 Go 开发者而言,保持与 Go 一致的理念,给自定义的类型一个合理的零值,并坚持保持自定义类型是零值可用的,这样我们的 Go 代码会表现的更加符合 Go 惯用法。

收藏
评论区

相关推荐

Go语言字符串和数值转换
一.字符串概述 字符串是一段不可变的字符序列.内容是任意内容,可以是一段文字也可以是一串数字,但是字符串类型数字不能进行数学运算,必须转换成整型或浮点型 字符串类型关键字:string 创建字符串类型变量 go var s string "hello,world" s1 : "hello,world" 字符串类型的值使用双引号""扩上
【Golang】Goland使用介绍
goland介绍 Goland官方地址:http://www.jetbrains.com/go/(http://www.jetbrains.com/go/) goland安装 下载 Windows下载地址:https://download.jetbrains.com/go/goland2018.2.1.exe(https://download
Golang中常用的字符串操作
Golang中常用的字符串操作 一、标准库相关的Package go import( "strings" ) 二、常用字符串操作 1. 判断是否为空字符串 1.1 使用“”进行判断 思路:直接判断是否等于""空字符串,由于Golang中字符串不能为 nil,且为值类型,所以直接与空字符串比较即可。 举例: go
Go语言开发的利与弊
Go 语言有多火爆?国外如 Google、AWS、Cloudflare、CoreOS 等,国内如七牛、阿里等都已经开始大规模使用 Go 语言开发其云计算相关产品。在 Go 语言的使用过程中,需要注意哪些 Yes 和 But? 最近,我们使用 Go 语言编写了一个 API,Go 语言是一种开源编程语言,2009 年由 Google 推出。在使用 Go 进行开
go 语言资源整理
Awesome GitHub Topic for Go(https://links.jianshu.com/go?tohttps%3A%2F%2Fgithub.com%2Ftopics%2Fgolang) Awesome Go(https://links.jianshu.com/go?tohttps%3A%2F%2F
[GO语言基础] 一.为什么我要学习Golang以及GO语言入门普及
作为网络安全初学者,会遇到采用Go语言开发的恶意样本。因此从今天开始从零讲解Golang编程语言,一方面是督促自己不断前行且学习新知识;另一方面是分享与读者,希望大家一起进步。这系列文章入门部分将参考“尚硅谷”韩顺平老师的视频和书籍《GO高级编程》,详见参考文献,并结合作者多年的编程经验进行学习和丰富,且看且珍惜吧!后续会结合网络安全进行GO语言实战深入,加
知乎从Python转为Go,是不是代表Go比Python好?
众所周知,知乎早在几年前就将推荐系统从 Python 转为了 Go。于是乎,一部分人就说 Go 比 Python 好,Go 和 Python 两大社区的相关开发人员为此也争论过不少,似乎,谁也没完全说服谁。 知乎从Python转为Go,是不是代表Go比Python好?我认为,各有优点,谁也取代不了谁,会长期共存! “由 Python 语言转向 Go 语言
go的三个运行基本命令的区别,go run ,go build 和 go install
最近在自学go,遇到点基础的问题,通过自己实际操作之后得出结论在实际操作之前,我们需要知道go有三种源码文件:      1,命令源码文件;声明自己属于main包,并且包含main函数的文件,每个项目只能有一个这样的文件,即程序的入口文件      2,库源码文件;不能直接被执行的源码文件      3,测试源码文件本次操作不涉及测试源码文件。go run
go语言开发入门:GO 开发者对 GO 初学者的建议
以促进 India 的 go 编程作为 GopherConIndia 承诺的一部分。我们采访了 40 位 Gophers(一个 Gopher 代表一个 GO 项目或是任何地方的 GO 程序员),得到了他们关于 GO 的意见。如果你正好刚刚开始 go 编程,他们对于我们一些问题的答案可能会对你有非常有用。看看这些。应该做:通读 the Go standard
Go语言学习——彻底弄懂return和defer的微妙关系
疑问前面在函数篇里介绍了Go语言的函数是支持多返回值的。只要在函数体内,对返回值赋值,最后加上return就可以返回所有的返回值。最近在写代码的时候经常遇到在return后,还要在defer里面做一些收尾工作,比如事务的提交或回滚。所以想弄清楚这个return和defer到底是什么关系,它们谁先谁后,对于最后返回值又有什么影响呢? 动手验证了解
Docker最全教程之Go实战,墙裂推荐(十八)
前言 与其他语言相比,Go非常值得推荐和学习,真香!为什么?主要是可以直接编译成机器代码(性能优越,体积非常小,可达10来M,见实践教程图片)而且设计良好,上手门槛低。本篇主要侧重于讲解了Go语言的优势,并且提供了一个推送钉钉消息的Demo。最后由于技
GO的执行原理以及GO命令
一、Go的源码文件 Go 的源码文件分类: yuanmawenjian1(https://imghelloworld.osscnbeijing.aliyuncs.com/b50e58692d24232e7d6437
一篇文章彻底弄懂go语言方法的本质
Go 语言不支持经典的面向对象语法元素,比如:类、对象、继承等。但 Go 语言也有方法(method)。和函数相比,Go 语言中的方法在声明形式上仅仅多了一个参数,Go 称之为 receiver 参数。而 receiver 参数正是方法与类型之间的纽带。Go 方法的一般声明形式如下:gofunc (receiver T/T) MethodName(参数列表)
go语言定义“零值可用”的类型
1. Go 类型的零值作为 C 程序员出身的我,我总是喜欢用在使用 C 语言的”受过的苦“与 Go 语言中得到的”甜头“做比较,从而来证明 Go 语言设计者在当初设计 Go 语言时是做了充分考量的。在 C99 规范中,有一段是否对栈上局部变量进行自动清零初始化的描述: 如果未显式初始化且具有自动存储持续时间的对象,则其值是不确定的。规范的用语总是晦涩难懂的。
理解go语言包导入路径的含义
Go 语言是使用包(package)作为基本单元来组织源码的,可以说一个 Go 程序就是由一些包链接在一起构建而成的。虽然与 Java、Python 等语言相比这算不上什么创新,但与祖辈 C 语言的头文件包含机制相比则是“先进”了许多。编译速度快是这种”先进性“的一个突出表现,即便是每次编译都是从零开始。Go 语言的这种以包为基本构建单元的构建模型使得依赖分