go语言定义零值可用的类型学习教程

 更新时间:2023年06月26日 11:11:50   作者:九路  
这篇文章主要为大家介绍了go语言定义零值可用的类型教程学习,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪

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语言定义零值可用的类型的详细内容,更多关于go 零值可用类型的资料请关注脚本之家其它相关文章!

相关文章

  • 一文带你吃透Golang中net/http标准库服务端

    一文带你吃透Golang中net/http标准库服务端

    这篇文章将从服务端(Server)作为切入点和大家分享一下Go语言net/http标准库的实现逻辑,进而一步步分析http标准库内部是如何运作的,感兴趣的可以了解下
    2024-03-03
  • 浅谈用Go构建不可变的数据结构的方法

    浅谈用Go构建不可变的数据结构的方法

    这篇文章主要介绍了用Go构建不可变的数据结构的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Go语言中日志的规范使用建议分享

    Go语言中日志的规范使用建议分享

    在任何服务端的语言项目中,日志是至关重要的组成部分,本文为大家整理了一些如何规范使用GO语言日志的建议,以及相应的实际示例,希望对大家有事帮助
    2024-01-01
  • GO语言入门Golang进入HelloWorld

    GO语言入门Golang进入HelloWorld

    本篇文章是go语言基础篇,非常适合go语言刚入门的小白,主要介绍了GO语言入门Golang进入HelloWorld,跟着小编一起来编写Go语言的第一程序helloworld吧
    2021-09-09
  • 在Go网络请求中配置代理的方法详解

    在Go网络请求中配置代理的方法详解

    这篇文章主要给大家介绍了如何在Go网络请求中配置代理的方法,文章通过代码示例介绍的非常详细,对大家的学习或工作有一定的帮助,需要的朋友可以参考下
    2023-09-09
  • Go语言使用MongoDB数据库详细步骤

    Go语言使用MongoDB数据库详细步骤

    mongodb是一种高性能、开源、文档型的nosql数据库,被广泛应用于web应用、大数据以及云计算领域,下面这篇文章主要给大家介绍了关于Go语言使用MongoDB数据库的详细步骤,需要的朋友可以参考下
    2024-05-05
  • Golang中常用的语法糖分享

    Golang中常用的语法糖分享

    语法糖,也称糖语法,是由英国计算机科学家彼得·兰丁提出的,用于表示编程语言中的某种类型的语法,这些语法不会影响功能,但使用起来却很方便,本文就来看看Golang中常用的语法糖有哪些吧
    2023-05-05
  • Golang连接PostgreSQL基本操作的实现

    Golang连接PostgreSQL基本操作的实现

    PostgreSQL是常见的免费的大型关系型数据库,本文主要介绍了Golang连接PostgreSQL基本操作的实现,具有一定的参考价值,感兴趣的可以了解一下
    2024-02-02
  • Go语言学习笔记之反射用法详解

    Go语言学习笔记之反射用法详解

    这篇文章主要介绍了Go语言学习笔记之反射用法,详细分析了Go语言中反射的概念、使用方法与相关注意事项,需要的朋友可以参考下
    2017-05-05
  • 使用golang在windows上设置全局快捷键的操作

    使用golang在windows上设置全局快捷键的操作

    最近在工作中,总是重复的做事,想着自己设置一个快捷键实现windows 剪贴板的功能,所以本文小编给大家分享了使用golang在windows上设置全局快捷键的操作,文中有相关的代码示例供大家参考,需要的朋友可以参考下
    2024-02-02

最新评论