标签 泛型 下的文章

Go语言演进的双保险:GOEXPERIMENT与GODEBUG

本文永久链接 – https://tonybai.com/2024/10/11/go-evolution-dual-insurance-goexperiment-godebug

Go语言自诞生以来就以其简洁、高效和强大的并发支持而闻名,Go团队承诺保持Go1向后兼容性,以确保用户的代码在未来的版本中继续正常运行。然而,保持语言的稳定性与不断创新(增加新特性)之间的平衡一直是Go团队面临的挑战。为了应对这一挑战,Go语言引入了两个关键机制:GOEXPERIMENT和GODEBUG来平衡新功能的试验、稳定发布和向后兼容。这两个机制共同构成了Go语言特性发布的“双保险”,确保语言能够稳步前进的同时,不会因为激进的改变而影响现有代码的稳定性。本文就来简单探讨一下这两个机制是如何保障Go语言新特性稳定发布的。

1. GOEXPERIMENT:新特性的摇篮

GOEXPERIMENT是一个Go语言的环境变量,是用于控制实验性特性的机制。它允许开发者在编译时(使用go build、go install、go run或go test)启用一些尚未正式发布的语言特性或优化。通过GOEXPERIMENT,Go团队能够在正式发布之前广泛测试新功能,收集反馈并进行必要的调整。

比如,在今年8月发布的Go 1.23版本发布了一个实验特性:带有类型参数的type alias,就像下面代码一样,我们可以在编译时开启该实验特性:

// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$GOEXPERIMENT=aliastypeparams go build generic_type_alias.go
$./generic_type_alias
Int Slice: [1 2 3 4 5]
String Slice: [hello world]
Person Slice: [{Alice 30} {Bob 25}]

如果不开启实验特性,上述的代码就会编译失败:

// github.com/bigwhite/experiments/blob/master/go1.23-examples/lang/generic_type_alias.go

$go build generic_type_alias.go
# command-line-arguments
./generic_type_alias.go:5:6: generic type alias requires GOEXPERIMENT=aliastypeparams

我们看到:通过设置GOEXPERIMENT=featureflag可以开启对应的实验特性,如果要同时开启多个实验特性,可以用逗号分隔的实验特性列表,就像下面这样:

$GOEXPERIMENT=featureflag1,featureflag2,...,featureflagN go build

那么如何查看当前Go版本有哪些实验验特性可用呢?我们可以借助go doc工具,以go 1.23.0为例:

$go doc goexperiment.Flags
package goexperiment // import "internal/goexperiment"

type Flags struct {
    FieldTrack        bool
    PreemptibleLoops  bool
    StaticLockRanking bool
    BoringCrypto      bool

    // RegabiWrappers enables ABI wrappers for calling between
    // ABI0 and ABIInternal functions. Without this, the ABIs are
    // assumed to be identical so cross-ABI calls are direct.
    RegabiWrappers bool
    // RegabiArgs enables register arguments/results in all
    // compiled Go functions.
    //
    // Requires wrappers (to do ABI translation), and reflect (so
    // reflection calls use registers).
    RegabiArgs bool

    // HeapMinimum512KiB reduces the minimum heap size to 512 KiB.
    //
    // This was originally reduced as part of PacerRedesign, but
    // has been broken out to its own experiment that is disabled
    // by default.
    HeapMinimum512KiB bool

    // CoverageRedesign enables the new compiler-based code coverage
    // tooling.
    CoverageRedesign bool

    // Arenas causes the "arena" standard library package to be visible
    // to the outside world.
    Arenas bool

    // CgoCheck2 enables an expensive cgo rule checker.
    // When this experiment is enabled, cgo rule checks occur regardless
    // of the GODEBUG=cgocheck setting provided at runtime.
    CgoCheck2 bool

    // LoopVar changes loop semantics so that each iteration gets its own
    // copy of the iteration variable.
    LoopVar bool

    // CacheProg adds support to cmd/go to use a child process to implement
    // the build cache; see https://github.com/golang/go/issues/59719.
    CacheProg bool

    // NewInliner enables a new+improved version of the function
    // inlining phase within the Go compiler.
    NewInliner bool

    // RangeFunc enables range over func.
    RangeFunc bool

    // AliasTypeParams enables type parameters for alias types.
    // Requires that gotypesalias=1 is set with GODEBUG.
    // This flag will be removed with Go 1.24.
    AliasTypeParams bool
}
    Flags is the set of experiments that can be enabled or disabled in the
    current toolchain.

    When specified in the GOEXPERIMENT environment variable or as build tags,
    experiments use the strings.ToLower of their field name.

    For the baseline experimental configuration, see objabi.experimentBaseline.

    If you change this struct definition, run "go generate".

go doc输出结果中的Flags结构体其实是$GOROOT/internal/goexperiment包中的一个类型,这个类型每一个字段对应一个实验特性,字段名的小写即可作为GOEXPERIMENT的值,比如AliasTypeParams的小写形式aliastypeparams正是我们在前面示例中使用的实验特性。

在Flags结构体中,我们看到了几个十分熟悉的字段,比如LoopVar、RangeFunc、Arenas等,这些实验特性有些已经正式落地,比如:Go 1.21引入的实验特性LoopvarGo 1.22版本中成为正式语法特性。而Arenas这个在Go 1.20版本引入的实验特性则因为实现上缺陷而迟迟不能转正,目前处于proposal hold状态

Go对实验特性的引入分为两种情况:

  • 默认开启实验特性,无需在编译时通过GOEXPERIMENT=featureflag显式开启

在Go 1.22中的exectracer2就是这样一个实验特性,它控制着是否使用新的execution trace的实现。

对于这样的实验特性,我们可以通过GOEXPERIMENT=nofeatureflag对其进行显式关闭,以Go 1.22引入的实验特性ExecTracer2为例,可以使用下面命令关闭该实验特性:

$GOEXPERIMENT=noexectracer2 go build

注:之后使用go version your-go-app,可以看到“your-go-app: go1.22.0 X:noexectracer2”的输出。

  • 默认不开启实验特性,需在编译时通过GOEXPERIMENT=featureflag显式开启

这就是我们最熟悉的实验特性引入方式,Go 1.23的AliasTypeParams实验特性就是默认不开启的,前面的例子已经给出了开发方法,这里就不赘述了。

实验特性通常经过1到2个版本的实验便会落地,成为正式特性。已经落地的实验特性通常会从Flags结构体中移除,比如Go 1.22的goexperiment.Flags结构体中的ExecTracer2,在Go 1.23中就看不到了。但总有一些已经落地的实验特性对应的flag字段依然还留存在Flags结构体里,比如:LoopVar,这个原因还不得而知!并且这样的已经成为正式特性的Flag,我们也无法再通过GOEXPERIMENT=nofeatureflag对其进行显式关闭了,因为它已经不再是实验特性了!

不过有些实验特性即便转正落地了,也会考虑到新特性对legacy code行为的影响而去读取go.mod中的go version再决定是否应用新特性,比如LoopVar。LoopVar转正后,该特性也仅在编译的包来自于包含声明Go 1.22或更高版本的模块时适用,比如:Go 1.22或Go 1.23。这可以确保没有程序会因为简单地采用新的Go版本而改变行为,我们来看一个例子:

// go.mod

module demo

go 1.20

// main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    var m = [...]int{1, 2, 3, 4, 5}

    for i, v := range m {
        go func() {
            time.Sleep(time.Second * 3)
            fmt.Println(i, v)
        }()
    }

    time.Sleep(time.Second * 5)
}

我们使用go 1.23.0版本编译该包,并运行输出的程序:

$go build
$./demo
4 5
4 5
4 5
4 5
4 5

可以看到,即便使用了Go 1.23版本,但因当前module的go version依然是go 1.20,Go编译器默认不会开启loopvar特性。

不过如果我们显式使用GOEXPERIMENT=loopvar,go编译器便不会考虑go.mod文件中的go version是什么版本,都会开启loopvar新特性:

$GOEXPERIMENT=loopvar go build
$./demo
4 5
1 2
0 1
2 3
3 4

Go编译器会有一套Go试验特性的默认值,如果你通过GOEXPERIMENT显式开启了某些特性,导致该特性flag值与默认值不同,那么我们可以通过go version命令查看到这些不同之处。以上面GOEXPERIMENT=loopvar go build构建出的demo为例:

$go version demo
demo: go1.23.0 X:loopvar

目前Go官方尚没有一个专门的页面用于汇总GOEXPERIMENT的各个flag的随Go版本release的历史,我们只能通过Flag字段在go issues查找其对应的issue来重温当时的情况。

到这里,我们可以看到GOEXPERIMENT引入的实验特性机制可以让Go团队相对稳健的向Go语言引入新特性(虽然不是所有新特性都需要走式样特性的流程,比如对泛型的支持等),但是当新特性破坏了向后兼容,或者Go团队要对现有特性的错误语义(比如panicnil)进行变更时,Go1这个严格的兼容性规则就很可能成为阻碍在大家面前的一道门槛!为了在保持兼容性和推动创新之间取得平衡,Go团队就需要一种新的机制,通过渐进式的方法来引入破坏性(break change)的变更,这就是GODEBUG控制机制,下面我们就来说说GODEBUG。

2. GODEBUG:在运行时控制特性行为的开关

GODEBUG也是一个Go环境变量,和GOEXPERIMENT用于构建时不同,GODEBUG用在运行时控制Go程序的某些行为。它允许开发者临时将某一特性恢复到旧的行为,即使在新版本中该特性的默认行为已经发生了改变。

GODEBUG的设置形式为逗号分隔的key=value对,例如:

$GODEBUG=http2client=0,http2server=0 ./your-go-app

这个设置会禁用客户端和服务器端对HTTP/2的使用。

上面是使用GODEBUG禁用新特性的例子。对于存量特性语义或实现变更,比如Go 1.23版本对time.Timer和Ticker进行了重实现,新实现底层使用了无缓冲channel,但通过下面设置可以恢复原先实现中的带缓冲channel:

$GODEBUG=asynctimerchan=1 ./your-go-app

考虑到兼容性而进行的GODEBUG设置将在至少两年(四个Go版本)内保持。但一些设置,例如http2client和http2server,将会更长时间地保持,甚至是无限期的。

除了GODEBUG环境变量之外,Go还提供了其他几种进行特性行为设置的方式,下面我们来看看。

3. GODEBUG、go:debug和go.mod中godebug directive的关系

3.1. //go:debug指令

从Go 1.21开始,可以在源代码中使用//go:debug指令来设置GODEBUG的值。这些指令必须放在文件的顶部,在package语句之前。例如:

//go:debug panicnil=1
//go:debug asynctimerchan=0
package main

这些指令会在编译时被处理,并影响生成的二进制文件的行为。

3.2 go.mod中的godebug指令

从Go 1.23开始,可以在go.mod文件中使用godebug指令来设置GODEBUG的默认值,例如:

// go.mod

godebug (
    default=go1.21
    panicnil=1
    asynctimerchan=0
)

这个配置会影响整个模块(module)的默认GODEBUG设置。

3.3 优先级和应用范围

那么GODEBUG、//go:debug以及go.mod中的godebug指令的优先级关系是怎样的呢?

显然,环境变量GODEBUG优先级最高,因为它可以在运行时覆盖其他设置,适用于临时调试或特定运行环境。

go:debug指令优先级次之,通常应用于特定的main包,适用于对特定程序进行精细控制。

而go.mod中的godebug指令优先级最低,为整个模块设置默认值,适用于项目级别的配置。

基于上述关系,我们来看看一个Go应用GODEBUG设置的默认值的确定过程。当没有显示设置GODEBUG环境变量时,各设置的默认值按以下顺序确定:

  • 首先查看用于构建程序的Go工具链(版本)的默认值。
  • 然后根据go.mod或go.work中声明的Go版本(go version)进行调整。
  • 之后应用go.mod中的godebug指令(如果有的话)。
  • 最后是//go:debug,通常仅应用于main module。

例如,如果一个项目的go.mod声明了go 1.20,那么即使使用Go 1.21工具链编译,也会默认使用panicnil=1(即允许panic(nil))。

不过有特殊情况需要注意,比如对于声明早于Go 1.20版本的项目,GODEBUG默认值会被配置为匹配Go 1.20的行为,而不是更早的版本;又比如在测试环境中,*_test.go文件中的//go:debug指令会被视为测试主包的指令等。

这么看规则还是蛮复杂的,那么编译后待执行的程序的默认GODEBUG的设置究竟是什么呢?我们可以通过go version -m来查看,以gopls v0.16.2为例:

$go version -m /Users/tonybai/Go/bin/gopls
/Users/tonybai/Go/bin/gopls: go1.23.0
    path    golang.org/x/tools/gopls
    mod golang.org/x/tools/gopls    v0.16.2 h1:K1z03MlikHfaMTtG01cUeL5FAOTJnITuNe0TWOcg8tM=
    dep github.com/BurntSushi/toml  v1.2.1  h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
    dep github.com/google/go-cmp    v0.6.0  h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
    dep golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338  h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y=
    dep golang.org/x/mod    v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0=
    dep golang.org/x/sync   v0.8.0  h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
    dep golang.org/x/telemetry  v0.0.0-20240829154258-f29ab539cc98  h1:Wm3cG5X6sZ0RSVRc/H1/sciC4AT6HAKgLCSH2lbpR/c=
    dep golang.org/x/text   v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
    dep golang.org/x/tools  v0.22.1-0.20240829175637-39126e24d653   h1:6bJEg2w2kUHWlfdJaESYsmNfI1LKAZQi6zCa7LUn7eI=
    dep golang.org/x/vuln   v1.0.4  h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I=
    dep honnef.co/go/tools  v0.4.7  h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs=
    dep mvdan.cc/gofumpt    v0.6.0  h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo=
    dep mvdan.cc/xurls/v2   v2.5.0  h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
    build   -buildmode=exe
    build   -compiler=gc
    build   DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1
    build   CGO_ENABLED=1
    build   CGO_CFLAGS=
    build   CGO_CPPFLAGS=
    build   CGO_CXXFLAGS=
    build   CGO_LDFLAGS=
    build   GOARCH=amd64
    build   GOOS=darwin
    build   GOAMD64=v1

我们看到其DefaultGODEBUG如下:

DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,httplaxcontentlength=1,httpmuxgo121=1,httpservecontentkeepheaders=1,panicnil=1,tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,x509keypairleaf=0,x509negativeserial=1

相对于GOEXPERIMENT的flags的数量,GODEBUG的设置项更多,下面我们根据go官方资料整理一个GODEBUG设置项列表供大家参考(信息截至2024.10.7)。

4. GODEBUG设置的历史演进

下表按照Go版本顺序列出了各个GODEBUG设置,包括它们被引入的版本、含义以及如何开启和关闭它们:

不过请注意以下几点:

  • 默认值可能会随着Go版本的更新而改变。
  • 某些设置可能在未来的Go版本中被移除。
  • 部分设置(如tlsmaxrsasize)允许指定具体的数值,而不仅仅是0或1。
  • 有些设置(如multipartmaxheaders和multipartmaxparts)在默认情况下是无限制的,需要明确设置一个数值来启用限制。

5. 小结

在Go语言的演进过程中,GOEXPERIMENT和GODEBUG两个机制起到了至关重要的作用。GOEXPERIMENT为新特性的实验和测试提供了灵活的环境,使得开发者可以在正式发布之前尝试和反馈新功能,从而确保Go语言的创新不会影响到已有代码的稳定性。通过这种方式,Go团队能够逐步引入新特性,同时维持向后兼容性。

另一方面,GODEBUG则为开发者提供了在运行时控制特性行为的工具,使得新版本引入的破坏性更改能够被临时禁用。这种灵活性使得开发者有一个平滑过渡的机会,能够在更新的同时,保证应用的平稳运行,避免了因语言更新而导致的潜在问题,使Go能够在保持稳定性的同时不断创新。

总的来说,这两个机制共同构成了Go语言特性发布的“双保险”,确保了语言的持续发展与稳定性之间的平衡。这一策略不仅促进了Go语言的创新,也增强了开发者的信心,使其能够在不断变化的环境中有效地编写和维护代码。

6. 参考资料


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

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://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

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

“类型名称”在Go语言规范中的演变

本文永久链接 – https://tonybai.com/2024/09/24/the-evolution-of-type-name-in-go-spec

Go语言规范(The Go Programming Language Specification)是Go语言的核心文档,定义了该语言的语法、类型系统和运行时行为。Go语言规范的存在使得开发者在实现Go编译器时可以依赖一致的标准,它确保了语言的稳定性和一致性,特别是在类型系统设计中,Go团队通过规范推动了语言的简洁性、稳定性与可维护性。对于Go开发者而言,Go语言规范也是语法特性使用的参考手册(虽然语言规范读起来比较抽象和晦涩)。

Go语言规范由Google的Go核心开发团队维护和演进,这与ISO标准的C/C++语言规范有所不同。C和C++语言的ISO标准更新较慢,需经过复杂的全球共识和审核流程,而相比之下,Go语言的管理方式就显得更加灵活,也能够迅速适应新需求。

然而,这种灵活性也带来了潜在的弊端。随着新语法特性的引入和演进,一些已有的概念的含义可能会发生变化,导致前后的不一致性,从而让开发者感到困惑。例如,Go中的Type Name(类型名称)就经历了从最初的Named Type,到Defined Type和Alias Type,最终又回归到Named Type的过程。

近期Go语言之父之一的Robert Griesemer在Go官博发表了一篇名为”What’s in an (Alias) Name?“的文章,其中就对Go spec中Type Name的历史演进做了回顾,这里我们就基于这段回顾对“类型名称(Type Name)”在Go语言规范中的演变做一下简要梳理,希望能帮助大家更好的理解Go。

1. Go规范中的Type Name(类型名称)

在Go语言规范中,Type Name是指给定类型的标识符,它为一个类型提供了唯一的名称。Type Name用于识别和引用各种类型,这包括Go内置(也叫预声明Predeclared Type)的基础类型(比如int、string)和用户自定义的类型,比如:

var x int       // int是基础类型的Type Name
type MyInt int  // MyInt是用户定义类型的Type Name

你可能会问,Go还有没有类型名称的类型吗?当然有了,有一些特殊的类型没有直接的类型名称。通常,这些类型是匿名类型(Anonymous Type),即它们并没有通过命名来标识,主要的匿名类型包括:

  • 字面量定义的复合类型(Composite Literals)

Go支持在代码中使用复合字面量来定义结构体、数组、切片、map等类型,而不为这些类型显式地定义名称。这些类型是在使用时定义的,并没有为其单独声明一个类型名称。

var data = struct { Name string; Age int }{"Alice", 30}  // 匿名结构体类型
var arr = [5]int{1,2,3,4,5} // 匿名数组类型
var arr = []int{1, 2, 3}  // 匿名切片类型
var m = map[string]int{"foo": 1, "bar": 2}  // 匿名map类型
  • 匿名函数类型

Go支持函数作为一等公民,函数本身可以作为类型,当定义匿名函数(即未命名函数)时,这些函数没有类型名称。

var f = func() int { //匿名函数类型func() int
    return 42
}

Type Name是一个广泛的概念,在Go spec中,Go设计者们将其做了细分,比如Named Type、Defined Type等。那么随着Go版本的变化,Go中的Type Name的分类有哪些重要的演进和变化呢,下面我们就重点说明一下Go spec中Type Name分类的三次重要变化。

2. 初始阶段:简单而明确的Named Type (2009-2017)

Go 1.0是Go语言的首个正式发布版本,其中确立了类型名称的基础概念。在这一阶段,Go的类型系统已经具备了高度的简洁性和一致性,这也是该语言设计的核心原则之一。

在Go语言的早期阶段(2009-2017),Go规范就确定了简单明确的Named Type的概念,它指的是通过下面语法定义的类型T:

type T existingType

这些通过类型声明定义的T被称为Named Type。而这里的existingType可以Predeclared的预声明类型(比如int、string),可以是已存在的Named Type,也可以是前面提到的匿名类型。

通过给现有类型赋予新名称来定义新的类型,与匿名类型等未命名类型形成鲜明对比。这种简单的分类满足了早期Go程序员的需求,为代码组织和类型系统提供了清晰的基础,提升了代码的可读性和模块化。

我们可以用示意图来展示这个阶段的Go类型名称分类:

而Named Type的定义方式也可以用下图表示:

我们看到,可以基于Predeclare Type、匿名类型以及已存在的Named Type来定义一个新的Named Type。并且,Named Type具有一些专有特性,比如可拥有自己的方法、只与自身类型赋值兼容,不与其底层类型直接兼容(除非进行显式类型转换)等。

3. 变革之始:别名类型的引入 (Go 1.9, 2017)

然而,随着Go 1.9在2017年引入别名类型(Alias Type),情况开始变得复杂:

type T = Q // T为Q类型的别名类型

别名类型的引入是为了支持大规模代码库的重构,但它也模糊了Named Type的界限,因为别名也是一个类型名称。

为了应对这一变化,Go团队引入了”Defined Type”的概念以代替界限模糊的Named Type,用以特指通过类型定义(type T Q)创建的新类型。

这样改动后,整个Go类型系统的类型名称分类就变成如下示意图中的状态了:

Defined Type定义和Alias Type的定义分别如下:

两者看起来差别不大,但只有Defined Type才拥有专有属性,比如可拥有自己的方法、只与自身类型赋值兼容等。我们也可以为Alias Type定义方法,但那个方法属于原类型。

4. 泛型时代的到来:概念的重塑 (Go 1.18, 2022)

2022年,Go 1.18的发布标志着Go语言进入了泛型时代,这一重大特性的引入再次挑战了现有的类型分类方式。

比如类型参数也是类型,它们有名称,与Defined Type一样,两个不同命名的类型参数表示不同的类型。换句话说,类型参数是Named Type,而且它们的行为在某些方面与Go原始的Named Type类似。更重要的是,Go的Predeclare Type(如int、string等)只能通过它们的名称来访问,并且像Defined Type和类型参数一样,如果它们的名称不同,它们也会不同,这样预声明的类型也变成了Named Type。

为了适应泛型,Go规范重新引入了Named Type,并将其范围扩大到包括预声明类型、Defined Type、类型参数以及部分情况下的别名类型。

重新引入Named Type后,Defined Type依然得以保留,整个Go系统类型的最新类型名称分类状态如下图所示:

5. 当前的权衡

在”What’s in an (Alias) Name?“的文章中,Robert还提到了学院派类型系统理论中的Nominal type(名义类型)和Structural type(结构类型)两个概念,虽然Go spec目前完全没有使用这两个概念。

Nominal type,也叫名义类型。这种类型的身份(identity)明确地与其名称相关联。两个类型即使结构完全相同,如果名称不同,也被视为不同的类型。像Go 1.18以后spec中的预声明类型(如int、string等)、Defined types(通过type关键字定义的类型)和类型参数都属于这种类型,这大体与Named Type是重叠的。

Structural type(结构类型)的类型的身份仅取决于其结构或组成,而不依赖于名称。如果两个类型的结构相同,它们就被视为相同的类型,即使它们可能有不同的名称,像Go中的接口类型(在某种意义上)、通过类型字面量创建的类型(如匿名结构体、函数类型等)等都可以归属与这种类型。值得注意的是,指向类型字面量的别名类型(如type AliasName = struct{ … })也可看作是structural type。

不过Robert也提到了,后续Go还会继续沿用Named Type、Defined Type等术语,而不会用这些学院派的类型术语来更新Go spec,这主要有几方面考虑:

  • 历史一致性:Go语言从早期就使用了named type、defined type等术语。突然改变可能会导致现有文档、教程和代码库的混乱。
  • 概念特殊性:Go的类型系统有其特殊性,不完全符合传统的nominal/structural二分法。例如,Go的接口类型结合了nominal和structural的特性。这么做,也可以避免引起其他语言中该术语用法的混淆。
  • 实用性考虑:”named type”、”defined type”等术语在Go的上下文中有明确的含义,直接对应于语言的特定特性和语法结构。这使得它们在讨论Go特定概念时更加实用。

6. 小结

本文基于Robert的文章讲述了Go语言类型系统中的类型名称的演变历程。我们回顾了Type Name在Go语言规范中的重要变化,从最初的简单Named Type到后来的Defined Type和Alias Type,再到引入泛型时代后的重新定义Named Type。每一次变化不仅反映了Go语言的不断发展,也展示了Go团队在应对复杂性和保持语言简洁性之间的平衡。


Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!

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://gopherdaily.tonybai.com

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite
  • Gopher Daily归档 – https://github.com/bigwhite/gopherdaily
  • Gopher Daily Feed订阅 – https://gopherdaily.tonybai.com/feed

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

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! 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