Go:值与指针

本文永久链接 – https://tonybai.com/2023/05/05/go-value-and-pointer

1. 计算机中的值

在百万年的演化历史中,人类对事物的属性进行了抽象,有了数量、精度、信息等概念的表示,对应的我们称之为整数、小数、文本文字等。计算机出现后,我们使用计算机对真实世界的问题进行建模,通过计算机的高效计算解决这些问题并输出答案。为了建模,计算机需要建立对上述基本概念的抽象和表示,于是有了类型与值的概念。

计算机中所有数据都存储在内存中并参与问题解决的计算,真实世界的概念表示与内存中的数据的转换关系如下图:

图中的有界比特序列(bounded bit sequence)就是真实世界概念表示在计算机内存中的存储形式,我们可以统称它为一个值(value)。这个值的比特序列形式由类型决定。举个例子:一个公司的员工数量为1000人,这个真实世界的概念在计算机中的表示过程如下:

我们用uint16类型来表示员工数量,这样它在内存存储形式为0000 0011 1110 1000。如果你用不同的类型来表示员工数量,那么在内存中表示员工数量的值的比特序列将是不同的。

反之,对于内存中的一段有界比特序列,在不同类型guided的decode下,得到的结果也是不同的,如下图。

我们看到:在uint64的guided下,0000 0011 1110 1000这个比特序列被解释为1000;而在[2]byte的guided下,0000 0011 1110 1000这个同样的比特序列则被解释成了2个数字。

计算机中的值不仅仅可以表示一个数字,也可以表示一个字符串,甚至是像结构体这样的复合类型,它本质上就是一块儿连续的内存,内存单元是有地址的,通过该地址访问和更新内存单元中的值。

但在编程过程中直接使用内存地址是十分不便的,因此在高级编程语言中,编程语言通过具名的标识符与内存单元建立“绑定”关系,就得到了我们通常说的常量和变量,而内存单元中存储的数据(即值)也可说成是常量持有的数据和变量持有的数据。

当然也有一些不和任何标识符“绑定”的值,我们称之为字面值(literal value)。我们通常用字面值为变量和常量赋[初]值:

var a int = 17
s := "hello"
const f float64 = 3.1415926

原生类型的字面值,可以简单理解为汇编中的立即数;而复杂类型(比如结构体)的字面值,则一般是临时存储在栈上的有界比特序列。

2. 一切皆是值

根据上一节关于值的定义,我们可以认为:在Go语言中,所有东西都是以值的形式存在的。在Go语言中,不仅仅是基本类型如整数、浮点数、布尔值等,就连复杂的数据结构,如结构体、数组、切片、map、channel等都以值的形式存在。

到这里有小伙伴可能会问:“不对啊,map、channel等应该是指针吧”。别急,要解答这个问题,我们就要来看看值的分类。

2.1 值的分类

在Go中,值可分为以下几种类型:

  • 基本类型值

基本类型是Go语言中最基础的数据类型,它们是直接由语言定义的。基本类型的值通常是简单的值,比如整数、浮点数、布尔值等。在Go语言中,基本类型的值可以进行各种运算和比较操作。

  • 复合类型值

复合类型则是由基本类型组成的更复杂的数据类型。它们的值由多个基本类型值组合而成,并且可以使用结构化的方式进行访问和操作。在Go语言中,复合类型包括分为数组、切片、map、结构体、接口、channel等多种类型。这些复合类型在不同的场景下都有不同的用途,可以用于表示不同的数据结构或者实现不同的算法。

字符串在Go中是一个特殊的存在,从Go类型角度来看,它应该属于原生内置的基本类型,但从值的角度考虑,由于在运行时字符串类型表示为一个两字段的结构(如下)

type StringHeader struct {
    Data uintptr
    Len  int
}

因此,我们将其归为复合类型值范畴。

  • 指针类型值

有一类值十分特殊,它自身是一个基本类型值,更准确的说是一个整型值,但这个整型值的含义却是另外一个值所在内存单元的地址。如下图所示:

我们看到:指针类型值为0×12345678,这个值是另外一个内存块(值为0×17)的地址。指针类型值在Go语言以及C、C++这一的静态语言中扮演着极其重要的角色。

回答前面小伙伴的问题:map、channel是不是值? 是值,只不过是指针类型值。从Go语法上来说,map、channel是某个runtime指针类型的实例。

2.2 值的可变性

在继续深入指针之前,我们先来插播一个内容:值的可变性

前面说过值是一段连续内存,是一个有界比特序列。原理上来说,内存中的值都是可变的。但现实中,考虑到操作系统管理以及应用安全的需要,暴露给开发人员的值被做了限定,即有些值(内存单元中的数据)是可变的,而有一些值是不可变的。

首先,操作系统负责物理内存与虚拟内存的映射,应用开发人员面对的是平坦的虚拟内存。这部分平坦的虚拟内存也被分为了几个段(segment),比如:BSS段、数据段、代码段、堆栈等,有些segment上的值是只读的,不可变的,比如代码段,有些则是可读写的可变的,比如堆栈。

此外,Go在编程语言层面也对值做了限制,常量值是不可变的,字符串类型值是不可变的,其他则为可变值。

2.3 指针类型

针对指针这类值,编程语言抽象出了一种类型:指针类型,指针类型的变量与指针类型值绑定,它内部存储的是另外一个内存单元的地址。这样就衍生出通过指针读取和更新指针指向的值的操作方法:

var a int = 5 // 基础类型值
var p = &a    // p为指针类型变量(*int),其值为变量a的地址。

println(*p)   // 通过指针读取其指向的变量a的值
*p = 15       // 通过指针更新其指向的变量a的值

不过,指针更大的好处在于传递开销低,且传递后,接收指针的函数/方法体中依然可以修改指针指向的内存单元的值。

接下来,我们来详细说一下值的传递。

2.4. 值的传递

无论是赋值还是传参,Go语言中的所有值的传递的方法都是值拷贝,也称为逐位拷贝(bitwise copy)

不过即便是值拷贝,也会带来三种不同效果:

  • 传值:你是你,我是我

效果:传递前后的变量各自独立更新,互不影响。

示例:传整型、浮点型、布尔值等。

  • 传指针:你是你,我是我,但我们共同指向他

效果:传递前后的指针变量拥有相同的指针值,因此共同指向同一个内存对象(d)。通过其中一个指针变量对指向的内存对象进行更新后(e),另一个指针变量可以感知到相同的变化。

示例:传*T指针类型变量。包括在Go runtime层面本质是一个指针的类型,比如map、channel等。

  • 传“引用”:你是你,我是我,但我们有一部分共同指向他

首先要注意,Go语言规范中没有“引用类型”这一表述。其次,也不要将这里的“引用”与其他语言的“引用类型”相提并论。

这里传“引用”的效果是:传递前后的变量一部分是独立更新互不影响的,一部分则是有共同指向,相互影响的。最典型的例子就是切片。当我们将切片传入函数后,函数内对切片的更新操作会影响到原切片,包括更新切片元素的值、向切片追加元素等。尤其是向切片追加(append)元素后,会导致传递前后的两个切片出现“不一致”,详情可以参考我之前写的一篇文章《当函数设计遇到切片》

这里之所以使用的“引用”来形容这种效果,主要是像slice这样的类型与我们熟知的其他语言中的引用(reference)很像,都是它们以“值”的形态传递,但却能干着“指针”的活儿。

3. 关于值的一些tips

3.1 零值

在Go语言中,每个变量都有一个默认的零值,即在变量未被初始化时的默认值。这个默认值取决于变量的类型,可以是一个数字、布尔值、字符串、指针、数组、结构体等等。

在Go语言中,零值可以用来初始化变量的默认值,也可以用来清空变量的值。

var i int // i的零值为0
var s string // s的零值为""
var p *int // p的零值为nil
var a [3]int // a的零值为[0 0 0]
var b struct { x int; y float64 } // b的零值为{0 0.0}

在这个例子中,我们使用var关键字声明了5个变量,并使用它们的零值来初始化这些变量的值。

另外,我们可以使用零值来清空变量的值,例如:

var i int = 10 // 初始化i的值为10
i = 0 // 使用i的零值来清空它的值

在使用零值时,需要注意以下两个问题:

  • 指针类型的零值为nil,不能直接使用nil指针来访问变量的值,否则会导致panic。
  • 可声明零长度数组类型,这样的类型的实例不占用内存空间,这在一些特殊场合下会很有用。

3.2 值的比较

Go语言的值比较是通过比较两个值的二进制表示来实现的。在Go语言中,值比较主要用于判断两个值是否相等。下面是Go语言值比较的场景、规则和注意事项:

场景

  • 判断两个值是否相等;
  • 判断两个值是否不相等;
  • 判断一个值是否为nil;
  • 判断两个指针是否指向同一个对象。

规则

  • 对于基本类型(如int、float、bool等),只需要比较它们的值就可以了;
  • 对于复合类型(如数组、切片、map等),需要递归比较它们的元素或键值对;
  • 对于结构体类型,需要递归比较它们的字段;
  • 对于接口类型,需要判断它们是否指向同一个动态类型以及动态值是否相等。

注意事项

  • 对于浮点数类型,不能使用“==”运算符进行比较,因为浮点数的精度问题可能导致比较结果不正确,应该使用math包中的函数进行比较;
  • 对于切片类型,Go不支持直接使用“==”运算符进行比较,因为它们的底层数据结构可能不同,应该使用reflect包中的函数DeepEqual进行比较;
  • 对于结构体类型,如果其中包含不可比较的字段(如切片、映射、函数等),则整个结构体类型也是不可比较的;
  • 对于指针类型,需要注意空指针的情况,应该先判断指针是否为nil,再进行比较。

3.3 method receiver的值与指针类型的选择

在Go语言中,method receiver可以是值类型或指针类型。这个选择可能会影响代码的性能、正确性和可读性等方面。

当一个方法的receiver是一个值类型时,receiver的传递会出现“传值”效果,方法体中对这个值的修改不会影响原来的值。但是,如果这个值类型的对象非常大,每次调用方法都需要进行复制,这会导致一定的性能损失。

当一个方法的receiver是一个指针类型时,这个方法操作的就是原来的对象,并且可以修改原来的对象。这种方式可以避免复制对象的开销,并且可以访问和修改对象的内部状态。但是,如果多个goroutine同时访问同一个对象时,就会发生竞争条件,导致程序出现不可预料的行为。

在选择method receiver的类型时,可考虑以下几个因素:

  • 对象的大小:如果对象很小,可以选择值类型的method receiver,避免复制对象的开销;如果对象很大,可以选择指针类型的method receiver,避免复制整个对象的开销。
  • 对象的可变性:如果对象需要被修改,应该选择指针类型的method receiver;如果对象不需要被修改,可以选择值类型的method receiver,保证代码的可预测性和可读性。
  • 对象类型或对象的指针类型是否需要实现特定的接口。

注:关于method receiver的类型选择问题,在《Go语言第一课》专栏的第25讲有系统的讲解。

3.4 使用unsafe.Pointer进行不同type guided的值decode

前面说过,值是一个“有界比特序列”,在不同类型guided的decode下,得到的结果也是不同的。我们可以通过unsafe.Pointer来进行不同的decode,比如下面例子将一个uint32的值重新分别decode为一个[2]uint16和[4]uint8数组:

package main

import (
    "fmt"
    "unsafe"
)

func main() {
    var a uint32 = 0x12345678

    b := (*[2]uint16)(unsafe.Pointer(&a))
    c := (*[4]uint8)(unsafe.Pointer(&a))

    fmt.Println(*b) // [22136 4660]
    fmt.Println(*c) // [120 86 52 18]
}

4. 小结

本文对Go语言中值做了重新解读,我们认为Go中的值就是一个有界比特序列(bounded bit sequence),是真实世界概念表示在计算机内存中的存储形式。

围绕着值这个概念,我们指出Go中一切皆是值。在这一观点的基础上,重新了解了值的分类、值的可变性、指针类型以及重要的值的传递,学习了值的传递的本质:bitwise-copy,以及这个传递过程针对不同类型值所取得的不同效果。

最后,我们了解了一些与值有关的tips,包括零值、值比较、method receiver类型选择以及值decode。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.21新特性前瞻

本文永久链接 – https://tonybai.com/2023/04/26/go-1-21-foresight

Go 1.21版本正在如火如荼地开发当中,按照Go核心团队的一年两次的发布节奏来算,Go 1.21版本预计将在2023年8月发布(Go 1.20版本是在2023年2月份发布的)。

本文将和大家一起看看Go 1.21都会带来哪些新特性。不过由于目前为时尚早,下面列出的有些变化最终不一定能进入到Go 1.21的最终版本中,所以切记一切变更要以最终Go 1.21版本发布时为准。

在细数变化之前,我们先来看看Go语言的当前状态。

1. Go语言当前状态

《2022年Go语言盘点》一文中,我们提到年初Go语言的2022年终排名为12位,同时TIOBE官方编辑也提到:“在新兴编程语言中,Go是唯一一个可能在未来冲入前十的后端编程语言”。Go语言的发展似乎应验了这一预测,在今年的3月份,Go就再次进入编程语言排行榜前十:

一个月后的四月初,TIOBE排行榜上,Go稳住了第10名的位次:

在国内,在鹅厂前不久发布的《2022年腾讯研发大数据报告》中,

在国内,继Go在2021年从C++手中夺过红旗首次登顶鹅厂最热门编程语言之后,在鹅厂前不久发布的《2022年腾讯研发大数据报告》中,Go蝉联鹅厂最热门编程语言,继续夯实在国内头部互联网公司内的优势地位:

Go于2009年开源,在经历多年的宣传和鼓吹后,Go目前进入了平稳发展的阶段。疫情结束后,原先线上举办或取消的国内外的Go技术大会现在陆续又都开始恢复了,相信这会让更多开发人员接触到Go。像Go这样的能在世界各地持续多年举办技术大会的语言真是不多了。

接下来,我们就来聚焦到Go 1.21版本,挖掘一下这个版本都有哪些新特性。

2. 语言变化

目前Go 1.21版本里程碑中涉及语言变化的有大约2项,我们来看看。

2.1 增加clear预定义函数

Go 1.21增加了一个clear预定义函数用来做切片和map的clear操作,其原型如下:

// $GOROOT/src/builtin.go

// The clear built-in function clears maps and slices.
// For maps, clear deletes all entries, resulting in an empty map.
// For slices, clear sets all elements up to the length of the slice
// to the zero value of the respective element type. If the argument
// type is a type parameter, the type parameter's type set must
// contain only map or slice types, and clear performs the operation
// implied by the type argument.
func clear[T ~[]Type | ~map[Type]Type1](t T)

clear是针对map和slice的操作函数,它的语义是什么呢?我们通过一个例子来看一下:

package main

import "fmt"

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6}
    fmt.Printf("before clear, sl=%v, len(sl)=%d, cap(sl)=%d\n", sl, len(sl), cap(sl))
    clear(sl)
    fmt.Printf("after clear, sl=%v, len(sl)=%d, cap(sl)=%d\n", sl, len(sl), cap(sl))

    var m = map[string]int{
        "tony": 13,
        "tom":  14,
        "amy":  15,
    }
    fmt.Printf("before clear, m=%v, len(m)=%d\n", m, len(m))
    clear(m)
    fmt.Printf("after clear, m=%v, len(m)=%d\n", m, len(m))
}

运行该程序:

before clear, sl=[1 2 3 4 5 6], len(sl)=6, cap(sl)=6
after clear, sl=[0 0 0 0 0 0], len(sl)=6, cap(sl)=6
before clear, m=map[amy:15 tom:14 tony:13], len(m)=3
after clear, m=map[], len(m)=0

我们看到:

  • 针对slice,clear保持slice的长度和容量,但将所有slice内已存在的元素(len个)都置为元素类型的零值;
  • 针对map,clear则是清空所有map的键值对,clear后,我们将得到一个empty map。

2.2 改变panic(nil)语义

使用defer+recover捕获panic是Go语言唯一处理panic的方法,其典型模式如下:

package main

import "fmt"

func foo() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Printf("panicked: %v\n", err)
            return
        }
        fmt.Println("it's ok")
    }()

    panic("some error")
}

func main() {
    foo()
}

运行上面程序会输出:

panicked: some error

例子中我们向panic传入了表示panic原因的字符串,panic的参数是一个interface{}类型,可以传入任意值,当然也可以传入nil

比如上面例子,当我们给foo函数的panic调用传入nil时,我们将得到下面结果:

it's ok

这可能会给开发者带去疑惑:明明是触发了panic,但函数却按照正常逻辑处理!2018年,前Go核心团队成员bradfitz就提出了一个issue:spec: guarantee non-nil return value from recover,提出当开发者调用panic(nil)时,recover应该返回某种runtime error,而不是nil。这个issue在今年被纳入了Go 1.21版本,现在该issue的实现已经被merge到了主干。

新的实现在src/runtime/panic.go中定义了一个名为PanicNilError的新Error:

// $GOROOT/src/runtime/panic.go

// A PanicNilError happens when code calls panic(nil).
//
// Before Go 1.21, programs that called panic(nil) observed recover returning nil.
// Starting in Go 1.21, programs that call panic(nil) observe recover returning a *PanicNilError.
// Programs can change back to the old behavior by setting GODEBUG=panicnil=1.
type PanicNilError struct {
    // This field makes PanicNilError structurally different from
    // any other struct in this package, and the _ makes it different
    // from any struct in other packages too.
    // This avoids any accidental conversions being possible
    // between this struct and some other struct sharing the same fields,
    // like happened in go.dev/issue/56603.
    _ [0]*PanicNilError
}

func (*PanicNilError) Error() string { return "panic called with nil argument" }
func (*PanicNilError) RuntimeError() {}

Go编译器会将panic(nil)替换为panic(new(runtime.PanicNilError)),这样我们用Go 1.21版本运行上面的程序,我们就会得到下面结果了:

panicked: panic called with nil argument

如果你的遗留代码中调用了panic(nil)(注:显然这不是一种很idiomatic的作法),升级到Go 1.21版本后你就要小心了。如果你想保留原先的panic(nil)行为,可以用GODEBUG=panicnil=1。

有童鞋可能会质疑这违反了Go1兼容性承诺,但实际上Go1兼容性规范保留了对语言规范中不一致或错误的修订权力,即便这种修订会导致遗留代码出现与原先不一致的行为。

3. 编译器与工具链

每个Go版本中,编译器和工具链的改动都不少,我们挑重点看一下:

3.1 一些OS的最小支持版本的更新

Go 1.21开始,go installer支持最小macOS版本更新为10.15,而最小Windows版本为Windows 10

3.2 低版本的go编译器将拒绝编译高版本的go module

从Go 1.21版本开始,低版本的go编译器将拒绝编译高版本的go module(go.mod中go version标识最低版本) ,这也是Russ Cox策划的Go扩展的向前兼容性提案的一部分。此外,Go扩展向前兼容性提案感觉比较复杂,可能不会全部在Go 1.21版本落地。

3.3 支持WASI

Go从1.11版本就开始支持将Go源码编译为wasm二进制文件,并在支持wasm的浏览器环境中运行。

不过WebAssembly绝不仅仅被设计为仅限于在Web浏览器中运行,核心的WebAssembly语言是独立于其周围环境的,WebAssembly完全可以通过API与外部世界互动。在Web上,它自然使用浏览器提供的现有Web API。然而,在浏览器之外,之前还没有一套标准的API可以让WebAssembly程序使用。这使得创建真正可移植的非Web WebAssembly程序变得困难。WebAssembly System Interface(WASI)是一个填补这一空白的倡议,它有一套干净的API,可以由多个引擎在多个平台上实现,并且不依赖于浏览器的功能(尽管它们仍然可以在浏览器中运行)。

Go 1.21将增加对WASI的支持,初期先支持WASI Preview1版本,之后会支持WASI Preview2版本,直至最终WASI API版本发布!目前我们可以使用GOOS=wasip1 GOARCH=wasm将Go源码编译为支持WASI的wasm程序,下面是一个例子:

// main.go
package main            

func main() {
    println("hello")
}

下载最新go dev版本后(go install http://golang.org/dl/gotip@latest),可以执行下面命令将main.go编译为wasm程序:

$ GOARCH=wasm GOOS=wasip1 gotip build -o main.wasm main.go

开源的wasm运行时有很多,wazero是目前比较火的且使用纯Go实现的wasm运行时程序,安装wazero后,可以用来执行上面编译出来的main.wasm:

$curl https://wazero.io/install.sh
$wazero run main.wasm
hello

3.4 Go 1.21可能推出纯静态工具链,不再依赖glibc

使用纯Go实现的net resolver,原先DNS的问题也将被解决,这样Go团队很可能在构建工具链的时候使用CGO_ENABLED=0构建出静态工具链,没有动态链接库的依赖。

3.5 go test -c支持为多个包同时构建测试二进制程序

Go 1.21版本之前,go test -c仅支持将单个包的测试代码编译为测试二进制程序,Go 1.21版本则允许我们同时为多个包构建测试二进制程序

下面是官方给出的例子:

$ go test -c -o /tmp ./pkg1 ./pkg2 ./pkg2
$ ls /tmp
pkg1.test pkg2.test pkg3.test

3.6 增加\$GOROOT/go.env

今天使用go env -w命令修改的默认环境变量会写入:filepath.Join(os.UserConfigDir(), “go/env”)。在Mac上,这个路径是\$HOME/Library/Application Support/go/env;在Linux上,这个路径是\$HOME/.config/go/env。

Go 1.21将增加一个全局层次上的go.env,放在\$GOROOT下面,目前默认的go.env为:

// $GOROOT/go.env

# This file contains the initial defaults for go command configuration.
# Values set by 'go env -w' and written to the user's go/env file override these.
# The environment overrides everything else.

# Use the Go module mirror and checksum database by default.
# See https://proxy.golang.org for details.
GOPROXY=https://proxy.golang.org,direct
GOSUMDB=sum.golang.org

我们仍然可以通过go env -w命令修改user级的env文件来覆盖上述配置,当然最高优先级的是OS用户环境变量,如果在OS用户环境变量文件(比如.bash_profile、.bashrc)中设置了Go的环境变量值,比如GOPROXY等,那么以OS用户环境变量为优先。

4. 标准库

我们接下来再来看看变更最多的一部分:标准库,我们将对主要变更项作简要介绍。

4.1 slices和maps进入标准库

Go 1.18版本泛型落地发布前的最后一刻,Rob Pike叫停了slices、maps等泛型包的入库,后来这两个包先放置在golang.org/x/exp下作为实验包。随着Go泛型日益成熟以及Go团队对泛型使用经验的增多,Go团队终于决定将golang.org/x/exp/slicesgolang.org/x/exp/maps在Go 1.21版本中将挪入标准库。

4.2 log/slog加入标准库

log/slog是Go官方版结构化日志包,大致与uber的zap包相当。在我之前的一篇文章《slog:Go官方版结构化日志包》有对slog的详尽说明,大家可以移步到那篇文章看看。不过slog的proposal依旧很多,后续slog可能会有持续改进和变更,与那篇文章中的内容可能会有一些差异。

4.3 sync包增加OnceFunc、OnceValue和OnceValues

在sync.Once的基础上,这个issue增加了三个与Once相关的”语法糖”API,用在一些对Once有需求的最常见的场景中。

4.4 增加testing.Testing函数

Go 1.21为testing包增加了func Testing() bool函数,该函数可以用来报告当前程序是否是go test创建的测试程序。使用Testing函数,我们可以确保一些无需在单测阶段执行的函数不被执行。比如下面例子来自这个issue:

// file/that/should/not/be/used/from/testing.go

func prodEnvironmentData() *Environment {
    if testing.Testing() {
        log.Fatal("Using production data in unit tests")
    }
    ....
}

4.5 一些变更点

5. 参考资料


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats