2025-01-23 03:24:06
本文永久链接 – https://tonybai.com/2025/01/23/the-hidden-details-of-go-exported-identifiers
前不久,在“Go+用户组”微信群里看到有开发者向七牛云老板许式伟反馈七牛云Go SDK中的某些类型没有导出,导致外部包无法使用的问题(如下图):
七牛开发人员迅速对该问题做出了“更正”,将问题反馈中涉及的类型saveasArgs和saveasReply改为了导出类型,即首字母大写:
不过,这看似寻常的问题反馈与修正却引发了我的一些思考。
我们大胆臆想一下:如果saveasReply类型的开发者是故意将saveasReply类型设置为非导出的呢?看一下“更正”之前的saveasReply代码:
type saveasReply struct {
Fname string `json:"fname"`
PersistenId string `json:"persistentId,omitempty"`
Bucket string `json:"bucket"`
Duration int `json:"duration"` // ms
}
有读者可能会问:那为什么还将saveasReply结构体的字段设置为导出字段呢?请注意每个字段后面的结构体标签(struct tag)。这显然是为了进行JSON 编解码,因为目前Go的encoding/json包仅会对导出字段进行编解码处理。
除了这个原因,原开发者可能还希望包的使用者能够访问这些导出字段,而又不想完全暴露该类型。我在此不对这种设计的合理性进行评价,而是想探讨这种做法是否可行。
我们对Go导出标识符的传统理解是:导出标识符(以大写字母开头的标识符)可以在包外被访问和使用,而非导出标识符(以小写字母开头的标识符)只能在定义它们的包内访问。这种机制帮助开发者控制类型和函数的可见性,确保内部实现细节不会被随意访问,从而增强封装性。
但实际上,Go的导出标识符机制是否允许在某些情况下,即使类型本身是非导出的,其导出字段依然可以被包外的代码访问呢?该类型的导出方法呢?这些关于Go导出标识符的细节可能是鲜少人探讨的,在这篇博文中,我们将系统地了解这些机制,希望能为各位小伙伴带来更深入的理解。
我们先回顾一下Go语言规范(go spec)对导出标识符的定义:
我们通常使用英文字母来命名标识符,因此可以将上述定义中的第一句理解为:以大写英文字母开头的标识符即为导出标识符。
注:Unicode字符类别Lu(Uppercase Letter)包含所有的大写字母。这一类别不仅包括英文大写字母,还涵盖多种语言的大写字符,例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而,我非常不建议大家使用非英文大写字母来表示导出标识符,因为这可能会挑战大家的认知习惯。
而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的,但并没有明确要求其关联的类型本身也必须是导出的。
这为我们提供了进一步探索Go导出标识符细节的机会。接下来,我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。
我们首先定义一个带有导出字段的非导出类型myStruct,并将它放在mypackage里:
// go-exported-identifiers/field/mypackage/mypackage.go
package mypackage
type myStruct struct {
Field string // 导出的字段
}
// NewMyStruct1是一个导出的函数,返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
return &myStruct{Field: value}
}
// NewMyStruct1是一个导出的函数,返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
return myStruct{Field: value}
}
然后我们在包外尝试访问myStruct类型的导出字段:
// go-exported-identifiers/field/main.go
package main
import (
"demo/mypackage"
"fmt"
)
func main() {
// 通过导出的函数获取myStruct的指针
ms1 := mypackage.NewMyStruct1("Hello1")
// 尝试访问Field字段
fmt.Println(ms1.Field) // Hello1
// 通过导出的函数获取myStruct类型变量
ms2 := mypackage.NewMyStruct1("Hello2")
// 尝试访问Field字段
fmt.Println(ms2.Field) // Hello2
}
在go-exported-identifiers/field目录下编译运行该示例:
$go run main.go
Hello1
Hello2
我们看到,无论是通过myStruct的指针还是实例副本,都可以成功访问其导出变量Field。这个示例的关键就是:我们使用了短变量声明直接通过调用myStruct的两个“构造函数(NewXXX)”得到了其指针(ms1)以及实例副本(ms2)。在这个过程中,我们没有在main包中显式使用mypackage.myStruct这个非导出类型。
采用类似的方案,我们接下来再看看是否可以在包外访问非导出类型的导出方法。
我们为非导出类型添加两个导出方法M1和M2:
// go-exported-identifiers/method/mypackage/mypackage.go
package mypackage
import "fmt"
type myStruct struct {
Field string // 导出的字段
}
// NewMyStruct1是一个导出的函数,返回myStruct的指针
func NewMyStruct1(value string) *myStruct {
return &myStruct{Field: value}
}
// NewMyStruct1是一个导出的函数,返回myStruct类型变量
func NewMyStruct2(value string) myStruct {
return myStruct{Field: value}
}
func (m *myStruct) M1() {
fmt.Println("invoke *myStruct's M1")
}
func (m myStruct) M2() {
fmt.Println("invoke myStruct's M2")
}
然后,试着在外部包中调用M1和M2方法:
// go-exported-identifiers/method/main.go
package main
import (
"demo/mypackage"
)
func main() {
// 通过导出的函数获取myStruct的指针
ms1 := mypackage.NewMyStruct1("Hello1")
ms1.M1()
ms1.M2()
// 通过导出的函数获取myStruct类型变量
ms2 := mypackage.NewMyStruct2("Hello2")
ms2.M1()
ms2.M2()
}
在go-exported-identifiers/method目录下编译运行这个示例:
$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
invoke *myStruct's M1
invoke myStruct's M2
我们看到,无论是通过非导出类型的指针,还是通过非导出类型的变量复本都可以成功调用非导出类型的导出方法。
提及方法,我们会顺带想到接口,非导出类型是否可以实现某个外部包定义的接口呢?我们继续往下看。
在Go中,如果某个类型T实现了某个接口类型I的方法集合中的所有方法,我们就说T实现了I,T的实例可以赋值给I类型的接口变量。
在下面示例中,我们看看非导出类型是否可以实现某个外部包的接口。
在这个示例中mypackage包中的内容与上面示例一致,主要改动的是main.go,我们来看一下:
// go-exported-identifiers/interface/main.go
package main
import (
"demo/mypackage"
)
// 定义一个导出的接口
type MyInterface interface {
M1()
M2()
}
func main() {
var mi MyInterface
// 通过导出的函数获取myStruct的指针
ms1 := mypackage.NewMyStruct1("Hello1")
mi = ms1
mi.M1()
mi.M2()
// 通过导出的函数获取myStruct类型变量
// ms2 := mypackage.NewMyStruct2("Hello2")
// mi = ms2 // compile error: mypackage.myStruct does not implement MyInterface
// ms2.M1()
// ms2.M2()
}
在这个main.go中,我们定义了一个接口MyInterface,它的方法集合中有两个方法M1和M2。根据类型方法集合的判定规则,*myStruct类型实现了MyInterface的所有方法,而myStruct类型则不满足,没有实现M1方法,我们在go-exported-identifiers/interface目录下编译运行这个示例,看看是否与我们预期的一致:
$go run main.go
invoke *myStruct's M1
invoke myStruct's M2
如果我们去掉上面代码中对ms2的注释,那么将得到Compiler error: mypackage.myStruct does not implement MyInterface。
接下来,我们再来考虑一个场景,即非导出类型用作嵌入字段的情况,我们要看看该非导出类型的导出方法和导出字段是否会promote到外部类型中。
我们改造一下示例,新版的带有嵌入字段的结构见下面mypackage包的代码:
// go-exported-identifiers/embedded_field/mypackage/mypackage.go
package mypackage
import "fmt"
type nonExported struct {
Field string // 导出的字段
}
// Exported 是导出的结构体,嵌入了nonExported
type Exported struct {
nonExported // 嵌入非导出结构体
}
func NewExported(value string) *Exported {
return &Exported{
nonExported: nonExported{
Field: value,
},
}
}
// M1是导出的函数
func (n *nonExported) M1() {
fmt.Println("invoke nonExported's M1")
}
// M2是导出的函数
func (e *Exported) M2() {
fmt.Println("invoke Exported's M2")
}
这里新增一个导出类型Exported,它嵌入了一个非导出类型nonExported,后者拥有导出字段Field,以及两个导出方法M1。我们也Exported类型定义了一个方法M2。
下面我们再来看看main.go中是如何使用Exported的:
// go-exported-identifiers/embedded_field/main.go
package main
import (
"demo/mypackage"
"fmt"
)
// 定义一个导出的接口
type MyInterface interface {
M1()
M2()
}
func main() {
ms := mypackage.NewExported("Hello")
fmt.Println(ms.Field) // 访问嵌入的非导出结构体的导出字段
ms.M1() // 访问嵌入的非导出结构体的导出方法
var mi MyInterface = ms
mi.M1()
mi.M2()
}
在go-exported-identifiers/embedded_field目录下编译运行这个示例:
$go run main.go
Hello
invoke nonExported's M1
invoke nonExported's M1
invoke Exported's M2
我们看到,作为嵌入字段的非导出类型的导出字段与方法会被自动promote到外部类型中,通过外部类型的变量可以直接访问这些字段以及调用这些导出方法。这些方法还可以作为外部类型方法集中的一员,来作为满足特定接口类型(如上面代码中的MyInterface)的条件。
Go 1.18增加了泛型支持,那么非导出类型是否可以用作泛型函数和泛型类型的类型实参呢?最后我们来看看这个细节。
和前面一样,我们先定义用于该示例的带有导出字段和导出方法的非导出类型:
// go-exported-identifiers/generics/mypackage/mypackage.go
package mypackage
import "fmt"
// 定义一个非导出的结构体
type nonExported struct {
Field string
}
// 导出的方法
func (n *nonExported) M1() {
fmt.Println("invoke nonExported's M1")
}
func (n *nonExported) M2() {
fmt.Println("invoke nonExported's M2")
}
// 导出的函数,用于创建非导出类型的实例
func NewNonExported(value string) *nonExported {
return &nonExported{Field: value}
}
现在我们将其用于泛型函数,下面定义了泛型函数UseNonExportedAsTypeArgument,它的类型参数使用MyInterface作为约束,而上面的nonExported显然满足该约束,我们通过构造函数NewNonExported获得非导出类型的实例,然后将其传递给UseNonExportedAsTypeArgument,Go会通过泛型的类型参数自动推导机制推断出类型实参的类型:
// go-exported-identifiers/generics/main.go
package main
import (
"demo/mypackage"
)
// 定义一个用作约束的接口
type MyInterface interface {
M1()
M2()
}
func UseNonExportedAsTypeArgument[T MyInterface](item T) {
item.M1()
item.M2()
}
// 定义一个带有泛型参数的新类型
type GenericType[T MyInterface] struct {
Item T
}
func NewGenericType[T MyInterface](item T) GenericType[T] {
return GenericType[T]{Item: item}
}
func main() {
// 创建非导出类型的实例
n := mypackage.NewNonExported("Hello")
// 调用泛型函数,传入实现了MyInterface的非导出类型
UseNonExportedAsTypeArgument(n) // ok
// g := GenericType{Item: n} // compiler error: cannot use generic type GenericType[T MyInterface] without instantiation
g := NewGenericType(n)
g.Item.M1()
}
但由于目前Go泛型还不支持对泛型类型的类型参数的自动推导,所以直接通过g := GenericType{Item: n}来初始化一个泛型类型变量将导致编译错误!我们需要借助泛型函数的推导机制将非导出类型与泛型类型进行结合,参见上述示例中的NewGenericType函数,通过泛型函数支持的类型参数的自动推导间接获得GenericType的类型实参。在go-exported-identifiers/generics目录下编译运行这个示例,便可得到我们预期的结果:
$go run main.go
invoke nonExported's M1
invoke nonExported's M2
invoke nonExported's M1
前面的诸多示例证明了:即使类型本身是非导出的,但其内部的导出字段以及它的导出方法依然可以在外部包中使用,并且在实现接口、嵌入字段、泛型等使用场景下均有效。
到这里,你可能会提出这样一个问题:会有Go开发者使用非导出类型结合导出字段或方法的设计吗?
其实这种还是很常见的,在Go标准库中就有不少,只不过它们更多是包内使用,类似于非导出类型xxxImpl和它的Wrapper类型XXX的关系,或是xxxImpl或嵌入到XXX中,就像这样:
// 包内实现
type xxxImpl struct { // 非导出的实现类型
// 内部字段
}
// 导出的包装类型
type XXX struct {
impl *xxxImpl // 包含实现类型
// 其他字段
}
// 或者通过嵌入方式
type XXX struct {
*xxxImpl // 嵌入实现类型
// 其他字段
}
但也有一些可以包外使用的,比如实现了某个接口,并通过接口值返回,提供给外部使用,例如下面的valueCtx,它实现了Context接口,并通过WithValue返回,供调用WithValue的外部包使用:
//$GOROOT/src/context/context.go
func WithValue(parent Context, key, val any) Context { // 构造函数,实现接口
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val any
}
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
这么做的目的是什么呢?大约有如下几点:
非导出类型的主要作用是防止外部直接使用和依赖其内部实现细节。通过限制类型的直接使用,库作者可以保持实现的灵活性,随时调整或重构类型的内部逻辑,而无需担心破坏外部调用代码; 还可以避免暴露多余的API,使库的接口更加简洁。
通过非导出类型,开发者还可以确保外部代码无法直接实例化类型,而必须通过导出的构造函数或工厂函数,就像前面举的示例那样。这种模式可以保证对象始终以特定的方式初始化,避免错误使用。同时,它还可以用来实现更复杂的初始化逻辑,如依赖注入或资源管理。
非导出类型可以用来实现导出的接口,从而将接口的实现细节完全隐藏。对于用户来说,只需要关心接口的定义,而无需关注其实现。
本文探讨了Go语言中的导出标识符及其相关细节,特别是非导出类型如何与其导出字段和导出方法结合使用。
尽管某些类型是非导出的,其内部的导出字段和方法依然可以在包外访问。此外,非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。这种设计不仅促进了封装和接口实现的灵活性,还允许开发者通过构造函数返回非导出类型的实例,从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节,简化外部接口,使得代码结构更加清晰。
本文涉及的源码可以在这里下载。
Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾
。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2025, bigwhite. 版权所有.
2025-01-22 18:37:43
本文永久链接 – https://tonybai.com/2025/01/22/gcflags-options-list-and-usage
Go build是Go开发中不可或缺的构建工具,其中-gcflags参数为开发者提供了向编译器传递额外选项的能力。然而,关于-gcflags的完整参数选项和使用模式,官方文档多有局限,很多开发者对此了解不深。本文将系统性地解析-gcflags的完整参数来源以及其结合包模式(package pattern)的使用方法,供大家参考。
注:本文主要以-gcflags为例,其实go build的-ldflags参数与-gcflags在使用方法上如出一辙,唯一不同的是ldflags是将参数传递给go链接器。
gcflags是Go构建工具的一个标志,用于向Go编译器 (go tool compile) 传递额外的编译参数。通过它,开发者可以调整编译行为,例如禁用优化、生成调试信息或输出反汇编代码等。
Go build文档中关于-gcflags的说明很短小精悍:
$go help build
... ...
-gcflags '[pattern=]arg list'
arguments to pass on each go tool compile invocation.
-ldflags '[pattern=]arg list'
arguments to pass on each go tool link invocation.
... ...
The -asmflags, -gccgoflags, -gcflags, and -ldflags flags accept a space-separated list of arguments to pass to an underlying tool during the build. To embed spaces in an element in the list, surround it with either single or double quotes. The argument list may be preceded by a package pattern and an equal sign, which restricts the use of that argument list to the building of packages matching that pattern (see 'go help packages' for a description of package patterns). Without a pattern, the argument list applies only to the packages named on the command line. The flags may be repeated with different patterns in order to specify different arguments for different sets of packages. If a package matches patterns given in multiple flags, the latest match on the command line wins. For example, 'go build -gcflags=-S fmt' prints the disassembly only for package fmt, while 'go build -gcflags=all=-S fmt' prints the disassembly for fmt and all its dependencies.
... ...
多数Go初学者初次看到上述关于gcflags的说明,都无法知道到底有哪些arg可用以及究竟如何使用gcflags,而Go cmd文档中关于gcflags的内容也仅限于上述这些。
我将大家遇到的主要问题总结为下面两条:
如果你能正确回答上述两个问题,那你就基本掌握了gcflags的使用,大可不必继续往下看了。
否则,我们就一起分别看一下这两个问题该如何解答。
在哪里能查找到gcflags可用的全部参数选项呢?go help build不行,go command的web文档中没有!甚至Go tool compile的web文档中列举的gcflag的参数列表也是不全的(或者说是文档没有及时同步最新的参数列表变化),也许我们应该提一个issue给Go团队^_^。
远在天边近在眼前!下面命令可以让-gcflag可用的参数选项完整列表尽收眼底:
$go tool compile -h
usage: compile [options] file.go...
-% debug non-static initializers
-+ compiling runtime
-B disable bounds checking
-C disable printing of columns in error messages
-D path
set relative path for local imports
-E debug symbol export
-I directory
add directory to import search path
-K debug missing line numbers
-L also show actual source file names in error messages for positions affected by //line directives
-N disable optimizations
-S print assembly listing
-V print version and exit
-W debug parse tree after type checking
-asan
build code compatible with C/C++ address sanitizer
-asmhdr file
write assembly header to file
... ...
同样,如果你要查看-ldflags的完整参数选项列表,你可以使用下面命令:
$go tool link -h
usage: link [options] main.o
-B note
add an ELF NT_GNU_BUILD_ID note when using ELF; use "gobuildid" to generate it from the Go build ID
-E entry
set entry symbol name
-H type
set header type
-I linker
use linker as ELF dynamic linker
-L directory
add specified directory to library path
-R quantum
set address rounding quantum (default -1)
-T int
set the start address of text symbols (default -1)
-V print version and exit
-X definition
add string value definition of the form importpath.name=value
-a no-op (deprecated)
-asan
enable ASan interface
... ...
到这里,我们得到了第一个问题的答案。
接下来,我们再来看第二个问题:-gcflags的使用模式。
根据go help build的输出,我们知道-gcflags的使用形式如下:
-gcflags '[pattern=]arg list'
其中:
对包模式有很好地理解并非是使用好gcflags的必要条件。但在一些复杂项目中,我们可能会通过包模式精确控制调试和优化,在这种情况下,对包模式有深入理解还是大有裨益的。
包模式是一种通过匹配规则指定目标包的方式,常见的包模式有几下几种:
基于上述关于gcflags使用形式以及包模式的说明,我们举几个示例来直观理解一下gcflags的用法:
$go build -gcflags=-S fmt
上述命令中的参数-S仅作用于fmt包,显示其反汇编代码。
$go build -gcflags='all=-N -l'
在Go module模式下,参数-N和-l应用于当前主模块所有包及其依赖,禁用优化和内联。
$go build -gcflags='fmt=-S' -gcflags='net/http=-N'
Go build命令行中可以多次使用-gcflags,上述命令中的第一个gcflags对fmt包启用反汇编输出(-S)。第二个gcflags对net/http包禁用优化(-N)。
$go build -gcflags='all=-N' -gcflags='fmt=-S'
像上面命令中,两个gcflags都匹配了fmt包,或者说两个gcflags的作用范围都包含了fmt包,这种情况下哪些参数会对fmt包生效呢?Go规定:当一个包匹配多个模式时,以最后一个匹配的参数为准。在这个例子中,fmt包将只应用-S参数,而其他包应用-N参数。
到这里,我们完成了对两个关于gcflags问题的回答!
最后小结一下:
通过本篇文章,希望你能掌握查看gcflags完整参数列表的方法以及gcflags的使用模式,并在构建和调试Go项目时能更加得心应手。
Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾
。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2025, bigwhite. 版权所有.
2025-01-14 06:45:22
本文永久链接 – https://tonybai.com/2025/01/14/understand-go-and-toolchain-in-go-dot-mod
Go语言自诞生以来,就一直将向后兼容性作为其核心理念之一。Go1兼容性承诺确保了为Go1.0编写的代码能够在后续的Go1.x版本中持续正确地编译和运行。这一承诺为Go的成功奠定了坚实的基础,它不仅保障了稳定性,也大大减轻了随着语言演进带来的代码维护负担。然而,兼容性的内涵并不仅限于向后兼容。向前兼容性,即旧版本的工具链能够优雅地处理针对新版本编写的代码,对于打造流畅的开发体验同样至关重要。
在Go 1.21版本之前,向前兼容性在某种程度上是一个被忽视的领域。尽管go.mod文件中的go指令可以标明模块预期的Go版本,但在实际中,它更像是一个指导性建议,而非强制性规则。旧版本的Go工具链会尝试编译那些需要较新版本的代码,这经常导致令人困惑的错误,更有甚者会出现“静默成功”的情况——代码虽然可以编译,但由于较新版本中的细微改动,其运行时行为可能并不正确。
Go 1.21的发布标志着这一现状的重大转变。该版本引入了健壮且自动化的工具链管理机制,将go指令转变为一项强制性要求,并简化了使用不同Go版本进行开发的工作流程。即将发布的Go 1.24版本在此基础上进一步增强,引入了tool指令,允许开发者指定对外部工具及其特定版本的依赖,从而进一步提升了代码的可重复性和项目的可维护性。
这些改进进一步明确和巩固了go命令作为全方位依赖管理器的角色定位,它不仅管理外部模块,还负责管理Go工具链版本,以及越来越多的外部开发工具(如下图):
不过向前兼容性规则的明确以及toolchain指令的引入也给Go开发者带来一定的理解上的复杂性,并且在使用Go 1.21版本之后,我们可能遇到会遇到一些因Go工具链版本选择而导致的编译问题。
本文将通过一系列典型场景和详细的示例,帮助读者全面理解Go向前兼容性的规则,以及go指令以及toolchain指令对Go工具链选择的细节,从而让大家能更加自信地驾驭Go开发中不断演进的技术环境。
接下来,我们就从对向前兼容性的理解开始!
向前兼容性,在编程语言的语境中,指的是旧版本的编译器或运行时环境能够处理针对该语言的新版本编写的代码。它与向后兼容性相对,后者确保的是新版本的语言能够处理为旧版本编写的代码。向后兼容性对于维护现有代码库至关重要,而向前兼容性则是在使用不断演进的语言和依赖项时获得流畅开发体验的关键所在。
向前兼容性的挑战源于新语言版本通常会引入新的特性、语法变更或对标准库的修改。如果旧的工具链遇到了依赖于这些新元素的代码,它可能无法正确地编译或解释这些代码。理想情况下,工具链应该能够识别出代码需要一个更新的版本,并提供清晰的错误提示,从而阻止编译或执行。
在Go 1.21之前的版本中,向前兼容性并没有得到严格的保证。让我们来看一个例子。我们用Go 1.18泛型语法编写一个泛型函数Print:
// toolchain-directive/demo1/mymodule.go
package mymodule
func Print[T any](s T) {
println(s)
}
// toolchain-directive/demo1/go.mod
module mymodule
go 1.18
如果你尝试使用Go 1.17版本来构建这个模块,你将会遇到类似以下的错误:
$go version
go version go1.17 darwin/amd64
$go build
# mymodule
./mymodule.go:3:6: missing function body
./mymodule.go:3:11: syntax error: unexpected [, expecting (
note: module requires Go 1.18
这些错误信息具有一定的误导性,它们指向的是语法错误,而不是问题的本质:这段代码使用了Go 1.18版本中才引入的泛型特性。虽然go命令确实打印了一条有用的提示(note: module requires Go 1.18),但对于规模大一些的项目来说,在满屏的编译错误中,这条提示很容易被忽略。
而比上面这个示例更隐蔽的问题是所谓的“静默成功”。
设想这样一个场景:Go标准库中的某个bug在Go 1.19版本中被修复了。你编写了一段代码,并在不知情的情况下依赖于这个bug修复。如果你没有使用任何Go 1.19版本特有的语言特性,并且你的go.mod文件中指定的是go 1.19,那么旧版本的Go 1.18工具链将会毫无怨言地编译你的代码并获得成功。然而,在运行这段代码时,你的程序可能会表现出不正确的行为,因为那个bug在Go 1.18的标准库中依然存在。这就是“静默成功”——编译过程没有任何错误提示,但最终生成的程序却是有缺陷的。
在Go 1.21版本之前,go.mod文件中的go指令更多的是一种指导性意见。它表明了期望使用的Go版本,但旧的工具链并不会严格执行它。这种执行上的疏漏是导致Go开发者面临向前兼容性挑战的主要原因。
Go 1.21版本从根本上改变了go指令的处理方式。它不再是一个可有可无的建议,而是一个强制性的规则。下面我们就来看看Go 1.21及更高版本中是如何确保向前兼容性的。由于多数情况下,我们不会显式在go.mod显式指定toolchain指令,因此,我们先来看看没有显式指定toolchain指令时,go指令对向前兼容性的影响。
Go 1.21对Go version、language version、release version等做了更明确的定义,我们先来看一下,这对后续理解go.mod文件中go指令的作用很有帮助。下图形象的展示了各个version之间的关系:
Go版本(Go Version),也是发布版本(Release Version)使用1.N.P的版本号形式,其中1.N称为语言版本(language version),表示实现该版本Go语言和标准库的Go版本的整体系列。1.N.P是1.N语言版本的一个实现,初始实现是1.N.0,也是1.N的第一次发布!后续的1.N.P成为1.N的补丁发布。
任何两个Go版本(Go version)都可以进行比较,以判断一个是小于、大于还是等于另一个。
如果语言版本不同,则语言版本的比较结果决定Go版本的大小。比如:1.21.9 vs. 1.22,前者的语言版本是1.21,后者语言版本是1.22,因此1.21.9 < 1.22。
如果语言版本相同,从小到大的排序为:语言版本本身、按R排序的候选版本(1.NrcR),然后按P排序的发布版本,例如:
1.21 < 1.21rc1 < 1.21rc2 < 1.21.0 < 1.21.1 < 1.21.2。
在Go 1.21之前,Go初始发布版本为1.N,而不是1.N.0,因此对于N < 21,排序被调整为将1.N放在候选版本(rc)之后,例如:
1.20rc1 < 1.20rc2 < 1.20rc3 < 1.20 < 1.20.1。
更早期版本的Go有beta发布,例如1.18beta2。Beta发布在版本排序中被放置在候选版本之前,例如:
1.18beta1 < 1.18beta2 < 1.18rc1 < 1.18 < 1.18.1。
有了上述对Go version等的理解,我们再来看看go.mod中go指令在向前兼容性规则中的作用。
Go 1.21及更高版本中,go.mod文件中的go指令声明了使用模块或工作空间(workspace)所需的最低Go版本。出于兼容性原因,如果go.mod文件中省略了go指令行(通常我们都不这么做),则该模块被视为隐式使用go 1.16这个指令行;如果go.work文件中省略了go指令行,则该工作空间被视为隐式使用go 1.18这个指令行。
那么,Go 1.21及更高版本的Go工具链在遇到go.mod中go指令行中的Go版本高于自身时会怎么做呢?下面我们通过四个场景的示例来看一下。
当前本地工具链go 1.22.0,go.mod中go指令行为go 1.23.0:
// toolchain-directive/demo2/scene1/go.mod
module scene1
go 1.23.0
执行构建:
$go build
go: downloading go1.23.0 (darwin/amd64)
... ...
Go自动下载当前go module中go指令行中的Go工具链版本并对当前module进行构建。
当前本地工具链go 1.22.0,go.mod中go指令行为go 1.22.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1:
// toolchain-directive/demo2/scene2/go.mod
module scene2
go 1.22.0
require (
github.com/bigwhite/a v1.0.0
)
replace github.com/bigwhite/a => ../a
执行构建:
$go build
go: module ../a requires go >= 1.23.1 (running go 1.22.0)
Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新,则会输出错误提示!
当前本地工具链go 1.22.0,go.mod中go指令行为go 1.22.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1,而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2:
// toolchain-directive/demo2/scene3/go.mod
module scene3
go 1.22.0
require (
github.com/bigwhite/a v1.0.0
github.com/bigwhite/b v1.0.0
)
replace github.com/bigwhite/a => ../a
replace github.com/bigwhite/b => ../b
执行构建:
$go build
go: module ../b requires go >= 1.23.2 (running go 1.22.0)
Go发现当前go module依赖的go module中go指令行中的Go版本比当前module的更新,则会输出错误提示!并且选择了满足依赖构建的最小的Go工具链版本。
当前本地工具链go 1.22.0,go.mod中go指令行为go 1.23.0,但当前module依赖的github.com/bigwhite/a的go.mod中go指令行为go 1.23.1,而依赖的github.com/bigwhite/b的go.mod中go指令行为go 1.23.2:
// toolchain-directive/demo2/scene4/go.mod
module scene4
go 1.23.0
require (
github.com/bigwhite/a v1.0.0
github.com/bigwhite/b v1.0.0
)
replace github.com/bigwhite/a => ../a
replace github.com/bigwhite/b => ../b
执行构建:
$go build
go: downloading go1.23.0 (darwin/amd64)
... ..
Go发现当前go module依赖的go module中go指令行中的Go版本与当前module的兼容,但比本地Go工具链版本更新,则会下载当前go module中go指令行中的Go版本进行构建。
从以上场景的执行情况来看,只有选择了当前go module的工具链版本时,才会继续构建下去,如果本地找不到这个版本的工具链,go会自动下载该版本工具链再进行编译(前提是GOTOOLCHAIN=auto)。如果像场景2和场景3那样,依赖的module的最低Go version大于当前module的go version,那么Go会提示错误并结束编译!后续你需要显式指定要使用的工具链才能继续编译!以场景3为例,通过GOTOOLCHAIN显式指定工具链,我们可以看到下面结果:
// demo2/scene3
$GOTOOLCHAIN=go1.22.2 go build
go: downloading go1.22.2 (darwin/amd64)
^C
$GOTOOLCHAIN=go1.23.3 go build
go: downloading go1.23.3 (darwin/amd64)
.. ...
我们看到,go完全相信我们显式指定的工具链版本,即使是不满足依赖module的最低go版本要求的!
想必大家已经感受到支持新向前兼容规则带来的复杂性了!这里我们还没有显式使用到toolchain指令行呢!但其实,在上述场景中,虽然我们没有在go.mod中显式使用toolchain指令行,但Go模块会使用隐式的toolchain指令行,其隐式的默认值为toolchain goV,其中V来自go指令行中的Go版本,比如go1.22.0等。
接下来我们就简单地看看toolchain指令行,我们的宗旨是尽量让事情变简单,而不是变复杂!
Go mod的参考手册告诉我们:toolchain指令仅在模块为主模块且默认工具链的版本低于建议的工具链版本时才有效,并建议:Go toolchain指令行中的go工具链版本不能低于在go指令行中声明的所需Go版本。
也就是说如果对toolchain没有特殊需求,我们还是尽量隐式的使用toolchain,即保持toolchain与go指令行中的go版本一致。
另外一个影响go工具链版本选择的是GOTOOLCHAIN环境变量,它的值决定了go命令的行为,特别是当go.mod文件中指定的Go版本(通过go或toolchain指令)与当前运行的go命令的版本不同时,GOTOOLCHAIN的作用就体现出来了。
GOTOOLCHAIN可以设置为以下几种形式:
local: 这是最简单的形式,它指示go命令始终使用其自带的捆绑工具链,不允许自动下载或切换到其他工具链版本。即使go.mod文件要求更高的版本,也不会切换。如果版本不满足,则会报错。
\<name> (例如go1.21.3): 这种形式指示go命令使用特定名称的Go工具链。如果系统中存在该名称的可执行文件(例如在PATH环境变量中找到了go1.21.3),则会执行该工具链。否则,go命令会尝试下载并使用名为\<name>的工具链。如果下载失败或找不到,则会报错。
auto(或local+auto): 这是默认设置。在这种模式下,go命令的行为最为智能。它首先检查当前使用的工具链版本是否满足go.mod文件中go和toolchain指令的要求。如果不满足,它会根据如下规则尝试切换工具链。
- 如果go.mod中有toolchain行且指定的工具链名称比当前默认的工具链更新,则切换到toolchain行指定的工具链。
- 如果go.mod中没有有效的toolchain行(例如toolchain default或没有toolchain行),但go指令行指定的版本比当前默认的工具链更新,则切换到与go指令行版本相对应的工具链(例如go 1.23.1对应go1.23.1工具链)。
- 在切换时,go命令会优先在本地路径(PATH环境变量)中寻找工具链的可执行文件,如果找不到,则会下载并使用。
\<name>+auto: 这种形式与auto类似,但它指定了一个默认的工具链\<name>。go命令首先尝试使用\<name>工具链。如果该工具链不满足go.mod文件中的要求,它会按照与auto模式相同的规则尝试切换到更新的工具链。这种方式可以用来设定一个高于内置版本的最低版本要求,同时又允许根据需要自动升级。
\<name>+path (或local+path): 这种形式与\<name>+auto类似,也指定了一个默认的工具链\<name>。不同之处在于,它禁用了自动下载功能。go命令首先尝试使用\<name>工具链,如果不满足要求,它会在本地路径中搜索符合要求的工具链,但不会尝试下载。如果找不到合适的工具链,则会报错。
大多数情况我们会使用GOTOOLCHAIN的默认值,即在auto模式下。但是如果在国内自动下载go版本不便的情况下,可以使用local模式,这样在本地工具链版本不满足的情况下,可以尽快得到错误。或是通过\<name>强制指定使用特定版本的工具链,这样可以实现对组织内采用的工具链版本的精准控制,避免因工具链版本不一致而导致的问题。
自go module诞生以来,我们始终可以使用go get对go module的依赖进行管理,包括添加/删除依赖,升降依赖版本等。
就像本文开头的那个图中所示,go命令作为全方位依赖管理器的角色定位,它不仅管理外部模块,还负责管理Go工具链版本,以及越来越多的外部开发工具。因此我们也可以使用go get管理指令行和toolchain指令行。
例如,go get [email protected] [email protected]将改变主模块的go.mod文件,将go指令行改为go 1.22.1,将toolchain指令行改为toolchain go1.24rc1。我们要保证toolchain指令行中的版本始终等于或高于go指令行中的版本。
当toolchain指令行与go指令行完全匹配时,可以省略和隐含,所以go get [email protected]时可能会删除toolchain行。
反过来也是这样,当go get [email protected]时,如果1.N.P < go指令行的版本,go指令行也会随之被降级为1.N.P,这样就和toolchain版本一致了,toolchain指令行可能会被删除。
我们也可以通过下面go get命令显式删除toolchain指令行:
$go get toolchain@none
通过go get管理Go指令行和toolchain指令行还会对require中依赖的go module版本产生影响,反之使用go get管理require中依赖的go module版本时,也会对Go指令行和toolchain指令行的版本产生影响!不过这一切都是通过go get自动完成的!下面我们通过示例来具体说明一下。
我们首先通过示例看看go get管理go指令行对require中依赖的Go模块版本的影响。
当你使用go get升级或降级go.mod文件中的go指令行时,go get 会根据新的Go版本要求,自动调整require指令行中依赖模块的版本,以满足新的兼容性要求。比如下面这个升级go版本导致依赖模块升级的示例。
假设你的模块mymodule的go.mod文件内容如下:
module example.com/mymodule
go 1.21.0
require (
example.com/moduleA v1.1.0 // 兼容Go 1.21.0
example.com/moduleB v1.2.0 // 兼容Go 1.21.0
)
example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本都只兼容到Go 1.21.0。
现在,你执行以下命令升级Go版本:
$go get [email protected]
go get会将go.mod文件中的go指令行更新为go 1.23.1。同时,它会检查require指令行中的依赖模块,发现example.com/moduleA和example.com/moduleB的v1.1.0和v1.2.0版本可能不兼容Go1.23.1。
假设example.com/moduleA和example.com/moduleB都有更新的版本v1.3.0,且兼容Go 1.23.1,那么go get会自动将require指令行更新为:
module example.com/mymodule
go 1.23.1
require (
example.com/moduleA v1.3.0 // 兼容Go 1.23.1
example.com/moduleB v1.3.0 // 兼容Go 1.23.1
)
如果找不到兼容Go 1.23.1 的版本,go get可能会报错,提示无法找到兼容新Go版本的依赖模块。
同理,降低go版本也可能触发require中依赖模块降级。我们来看下面示例:
假设你的模块mymodule的go.mod文件内容如下:
module example.com/mymodule
go 1.23.1
require (
example.com/moduleA v1.3.0 // 兼容 Go 1.22.0及以上
example.com/moduleB v1.3.0 // 兼容 Go 1.22.0及以上
)
现在,你执行以下命令降低go版本:
$go get [email protected]
执行以上命令后,go.mod文件内容变为:
module example.com/mymodule
go 1.22.0
require (
example.com/moduleA v1.1.0 // 兼容Go 1.21.0及以上
example.com/moduleB v1.2.0 // 兼容Go 1.21.0及以上
)
在这个例子中, go get [email protected]命令会将go指令行降级为go 1.22.0, 同时, go get会自动检查所有依赖项, 并尝试将它们降级到与go 1.22.0兼容的最高版本。在这个例子中, example.com/moduleA和example.com/moduleB都被降级到了与go 1.22.0兼容的最高版本。
反过来,使用go get管理require中依赖的Go模块版本时,也会对go指令行产生影响,我们看一个添加依赖导致go指令行版本升级的示例。
假设你的模块mymodule的go.mod文件内容如下:
module example.com/mymodule
go 1.21.0
require (
example.com/moduleA v1.1.0 // 兼容 Go 1.21.0
)
现在,你需要添加一个新的依赖项example.com/moduleC,而example.com/moduleC的最新版本v1.2.0的go.mod文件中指定了go 1.22.0:
// example.com/moduleC 的 go.mod
module example.com/moduleC
go 1.22.0
require (
...
)
你执行以下命令添加依赖:
$go get example.com/[email protected]
go get会发现example.com/moduleC的版本v1.2.0需要 Go 1.22.0,而你的模块当前只兼容Go 1.21.0。因此,go get会自动将你的模块的go.mod文件更新为:
module example.com/mymodule
go 1.22.0
require (
example.com/moduleA v1.1.0 // 兼容Go 1.21.0
example.com/moduleC v1.2.0 // 需要Go 1.22.0
)
go指令行被升级到了go 1.22.0,以满足新添加的依赖项的要求。
不过无论如何双向影响,我们只要记住一个原则就够了,那就是go get和go mod tidy命令使go指令行中的Go版本始终保持大于或等于任何所需依赖模块的go指令行中的Go版本。
本文深入探讨了Go语言在版本管理和工具链兼容性方面的重要变革,特别是Go 1.21及以后的版本如何强化向前兼容性。在文章里,我强调了向后兼容性和向前兼容性在开发体验中的重要性,以及如何通过go指令和新引入的toolchain指令来管理工具链版本。
通过文中的示例,我展示了如何在不同场景下处理Go模块的兼容性问题,并解释了GOTOOLCHAIN环境变量如何影响工具链选择。最后,我还举例说明了如何通过使用go get命令有效管理Go指令和依赖模块的版本,确保代码的可维护性和稳定性。
不过我们也看到了,为了实现精确的向前兼容,Go引入了不少复杂的规则,短时间内记住这些规则还是有门槛的,我们只能在实践中慢慢吸收和理解。
本文涉及的源码可以在这里下载。
Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们>将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“Go陷阱与缺陷”和“Go原理课”专栏!此外,我们还会加强星友之间的交流
和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾
。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格6$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2025, bigwhite. 版权所有.
2025-01-06 05:48:41
本文永久链接 – https://tonybai.com/2024/01/06/the-2024-review-of-go-programming-language
2024年底,由于感染了甲流,我在家卧床休息了两天,原定于2024年进行的Go语言盘点写作因此被迫推迟。不过,我始终相信:迟到但不会缺席。在2025年元旦的第一天,我终于开始了这篇博客的撰写。
时间过得真快,《2023年Go语言盘点:稳中求新,稳中求变》依然历历在目。转眼之间,一年365天过去了,发生了许多事情,甚至有些记忆已在脑海中模糊或消逝。在这里,我将带你盘点那些关于Go的重要时刻,唤起你对Go的美好回忆。
回顾整个2024年,如果非要用一句话来形容Go语言的状态,我会选择:Go完成了技术成熟度曲线中的“稳步爬升复苏期”,开始进入“生产成熟期”。这一点在Go的排名中得到了直接体现,并在Go社区的活跃度方面得到了间接的印证。而Go的年中换帅似乎也预示着这是一个新的起点!在过去一年中,得益于Go团队和社区的共同努力,Go发布了许多值得关注的新特性。
接下来,我将为大家逐一详细介绍!
说到编程语言排名,程序员们首先想到的就是TIOBE!在2024年的TIOBE排行榜上,尽管Go语言没有像AI时代的霸主语言Python那样耀眼,但跻身前十并站稳第七名这一成绩也足以让其他语言羡慕不已!
而从2009年开源至今,Go在TIOBE排名走势如下:
了解Go历史的朋友都知道,Go语言真正具备生产级成熟度是从2015年的Go 1.5版本开始的。按照技术成熟度曲线的划分,2015年之前及其后的一段时间可以视为技术萌芽期。从曲线中可以看出,2017年时达到了期望膨胀期的峰值。此后,Go经历了一段“漫长”的泡沫破裂低谷期以及稳步爬升的复苏期。从2023年开始,到2024年末,Go语言复苏的速度日益加快!目前来看,如无意外,Go将进入技术成熟度曲线的下一阶段:生产成熟期!我曾提到过:绝大多数主流编程语言将在其诞生后的第15至第20年间大步前进。按照这个编程语言的一般规律,刚刚迈过开源第15个年头的Go刚刚迈进自己的黄金5-10年。
当然,单看TIOBE单一榜单似乎说服力不足,我们再来看看今年的Github octoverse报告。在这份报告中,Go依旧稳居github热门编程语言前10(如下图),这一位置已经保持了三年多了!
此外,在2024年年中发布的“IEEE Spectrum 2024编程语言排行榜”中,Go在Spectrum排名和Trending排名中分列第8位和第7位。
除了排行榜之外,通过Reddit中编程语言论坛的活跃度也可以看出Go语言在全球的受欢迎程度和用户广度。以下是2025年1月1日Reddit上最活跃的9门编程语言子论坛的实时状态截图:
我们看到Go子论坛在成员数量和某一时刻的在线人数上都表现良好。此外,如果你是长期关注Reddit Go论坛的Gopher,一定注意到自2024年初以来,Go论坛的人气迅速增长,日均帖子数相比前两年显著增加,其中很多都是新加入Go阵营的初学者!
注:Rust的人气是真高啊,online人数断崖领先!
编程语言技术大会是衡量语言流行度和受欢迎程度的另一重要风向标。自从全球从新冠疫情中恢复后,GopherCon逐渐在各地线下恢复,到了2024年基本回到了疫情前的状态,甚至在一些地方的GopherCon还超越了以往的受欢迎程度。例如,2024年GopherCon欧洲大会破例举办了两次。此外,首届在非洲举行的GopherCon Africa也于2024年10月份在肯尼亚首都内罗毕成功举行!唯一的遗憾是GopherChina在2024年缺席,这或许与国内的经济形势有关。
Go的增长趋势来的有些快,不知道是否是得益于AI应用的快速发展!但就像Go团队前成员Jaana Dogan(Rakyll)所说的那样:
Go将成为AI时代重要的AI应用开发语言!AI大模型三强:OpenAI、Claude和Google都提供了对Go SDK的官方支持:
此外,提到Go和AI大模型,我们不得不提及一个重量级的开源项目——Ollama,它可以说是当前私有部署和使用开源大模型的事实标准!在2024年的用户调查报告中,Go团队还特别关注了用户对使用Go开发AI应用的需求,并将AI应用开发视为Go应用的下一个重要赛道。此外,Russ Cox也积极参与这一领域,开源了专用于开源项目运营维护的AI机器人:Oscar,同时探索Go在AI领域的应用。
如果说Go的排名再创新高让Gopher和Go社区对Go充满了更多自信,那么Go团队的换帅则向整个编程语言界展示了团队的传承与发展!
对于Go团队来说,2024年的最大的事件不是Go 1.22或Go 1.23的发布,而是团队换帅。
2024年中旬,Go团队的技术负责人Russ Cox宣布,他将于2024年9月1日起卸任Go项目的技术领导职务。自2008年参与Go项目以来,Russ于2012年成为其技术负责人。在过去的12年里,他引领Go语言从一个实验性项目成长为当今最受欢迎的编程语言之一。在他的带领下,Go凭借简洁的语法、高效的并发模型和强大的标准库赢得了众多开发者的青睐,并在云计算、微服务和DevOps等领域得到了广泛应用。
Russ分享了他卸任的想法,表示这一决定是经过深思熟虑的,是自然发展的结果。他认为,尽管长期稳定的领导对大型项目至关重要,但领导层的变动也能为项目注入新的活力和视角。他强调,定期更换领导者是非常重要的,这有助于引入新思想并防止项目陷入停滞。
接替Russ Cox的是Austin Clements,他将成为新的Go技术负责人,同时领导Google的Go团队和整个Go项目。Austin自2014年起就在Google从事与Go相关的工作,拥有丰富的经验和深厚的技术背景。同时,Cherry Mui将接手负责编译器和运行时等“Go核心”领域的工作。Cherry自2016年加入Google,在Go的核心开发领域表现出色。Russ Cox对这两位新领导给予了高度评价,称赞他们具备卓越的判断力以及对Go语言和其运行系统的广泛而深入的理解。
通过9月份到12月份的角色过期期的观察来看,两位“新负责人”的表现是中规中矩,沿袭了Russ Cox之前确定的Go项目管理框架,Cherry Mui在Go core领域表现的十分积极,这从”Go compiler and runtime meeting notes“的记录中可见一斑!
在第333期GoTime播客中,两位新leader也初步分享了他们对后续Go演进的一些想法。
Austin强调,虽然Go保持着稳定和简洁,但它必须继续演进。他的首要目标之一是改善Go的可扩展性,无论是在开发过程中还是在背后的工程流程中。他希望通过提高透明度和扩大社区参与度,赋能社区,创建一个能够更好整合用户反馈的平台(可能是一个论坛),使贡献者能够开发与核心团队目标一致的工具和解决方案。在性能改进方面,Austin长期致力于优化Go的垃圾回收系统,目前正在试验一种新算法,幽默地称其为“绿茶”,旨在优化资源使用,进一步提升Go在越来越大系统上的扩展能力。
Cherry则指出,Go的用户基础正在快速增长,而核心团队的资源却有限。她的任务是确保Go平台能够支持这一日益增长的社区,无论是通过构建更好的API还是平台,帮助用户在Go的基础上开发更强大的工具和解决方案。在技术扩展性方面,Cherry也表达了自己的关注。随着计算能力的提升,核心数量和内存容量不断增加,Go需要适应,以高效处理更大的工作负载。Cherry表示,她非常期待与社区中的工程师合作,解决这些挑战,保持Go简单且可扩展的声誉。
从两位领导的想法与目标中,我们可以看到Go团队传承的文化。对于这样的“换帅”,Go社区应充满信心。
对于已经过了15个生日的Go来说,其演进的节奏已经非常稳定和成熟了。2024年,Go平稳地发布了两个重要版本:Go 1.22和Go 1.23。下面我们就来简单浏览一下这两个版本的主要新特性。
type MySlice[T any] = []T
更多更详细关于Go新特性的内容,请阅读《Go 1.22中值得关注的几个变化》和《Go 1.23中值得关注的几个变化》。
按照Go演进的一贯风格,我本不该对Go抱有过多期待^_^,但还是忍不住想说几句。
Go已经稳稳地占据了云计算领域的头部后端编程语言地位,在多个编程语言排行榜上名列前茅,Go社区也在健康快速地发展。然而,机遇与风险总是并存。
虽然Go在云原生、Web服务、微服务、API和CLI开发方面拥有明显优势,但也面临着来自Rust等语言的挑战。Go需要进一步巩固其在这些优势领域的地位,同时探索一些能够发挥自身优势的新方向,例如AI应用开发等。
同时,我们期待新一代Go团队领导者,尤其是来自Go编译器和运行时组的领导者们,能够深入打磨和优化Go语言的编译器、运行时性能以及语言互操作性。毕竟,谁不喜欢那种因性能自然增长而带来的愉悦感,以及借助其他语言优势生态快速完成功能的灵活性呢!
最后,感谢Go团队和Go社区在Go语言演进发展上做出的贡献,希望Go越走越好!
Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2025, bigwhite. 版权所有.
2024-12-26 05:41:41
本文永久链接 – https://tonybai.com/2024/12/26/exploring-the-connection-establish-process-of-webrtc-app-built-with-pion
在《WebRTC第一课:从信令、ICE到NAT穿透的连接建立全流程》一文中,我们从理论层面全面细致地了解了WebRTC连接建立的完整流程。这个流程大致可以分为以下几个阶段:
这个过程的复杂性不言而喻。即便多次阅读全文,读者可能仍难以形成深入的理解。因此,如果能够配上一个真实的示例,相信会更有助于读者全面把握这一过程的细节和原理。
在这篇文章中,我就为大家呈现一个真实的示例,我将使用Go语言开源WebRTC项目pion/webrtc来实现一个基于datachannel的WebRTC演示版程序,通过将pion/webrtc的日志级别设置为TRACE级,输出更多pion/webrtc实现层面的日志,以帮助大家理解WebRTC建连过程。同时,我还会实现一个简易版的基于“Room抽象模型”的信令服务器,供WebRTC通信两端交换信息使用。希望该示例能帮助大家更好的理解WebRTC端到端的建连流程。
按照WebRTC建连的流程,我们先来实现一个简易版的信令服务器。
注:提醒各位读者,本文中所有例子均以演示和帮助大家理解为目的,不建议在生产中使用示例中的代码。
下面是一个基于WebSocket的WebRTC信令服务器的简化实现,使用WebSocket进行WebRTC信令交换可以提供更快速、更高效和更灵活的通信体验,同时WebSocket生态丰富,可复用的代码库有很多,实现起来也比较简单。
这个信令服务器是基于Room抽象模型的,因此其主要结构是一个Room结构体,代表一个聊天室。我们具体看一下该信令服务器的实现代码:
// webrtc-first-lesson/part2/signaling-server/main.go
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"github.com/gorilla/websocket"
)
type Room struct {
Clients map[*websocket.Conn]bool
mu sync.Mutex
}
var (
upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
rooms = make(map[string]*Room)
roomMu sync.Mutex
)
func main() {
http.HandleFunc("/ws", handleWebSocket)
log.Println("Signaling server starting on :28080")
log.Fatal(http.ListenAndServe(":28080", nil))
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println("Error upgrading to WebSocket:", err)
return
}
defer conn.Close()
remoteAddr := conn.RemoteAddr().String()
log.Println("New WebSocket connection from:", remoteAddr)
roomID := r.URL.Query().Get("room")
if roomID == "" {
roomID = fmt.Sprintf("room_%d", len(rooms)+1)
log.Printf("Created new room: %s\n", roomID)
}
roomMu.Lock()
room, exists := rooms[roomID]
if !exists {
room = &Room{Clients: make(map[*websocket.Conn]bool)}
rooms[roomID] = room
}
roomMu.Unlock()
room.mu.Lock()
room.Clients[conn] = true
room.mu.Unlock()
log.Printf("Client[%v] joined room %s\n", remoteAddr, roomID)
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
break
}
var msg map[string]interface{}
if err := json.Unmarshal(message, &msg); err != nil {
log.Println("Error unmarshaling message:", err)
continue
}
msg["roomId"] = roomID
updatedMessage, _ := json.Marshal(msg)
room.mu.Lock()
for client := range room.Clients {
if client != conn {
clientAddr := client.RemoteAddr().String()
if err := client.WriteMessage(messageType, updatedMessage); err != nil {
log.Println("Error writing message:", err)
} else {
log.Printf("writing message to client[%v] ok\n", clientAddr)
}
}
}
room.mu.Unlock()
}
room.mu.Lock()
delete(room.Clients, conn)
room.mu.Unlock()
log.Printf("Client[%v] left room %s\n", remoteAddr, roomID)
}
我们看到:Room结构体包含一个WebSocket连接的map和一个互斥锁。演示程序使用全局变量rooms(房间map)和相应的互斥锁管理房间和加入房间的连接,并在房间内进行消息广播,以保证消息能转发到参与通信的所有端(Peer)。当然,如果仅有两端在一个房间中,那么这就变成了一对一的实时通信。
这个信令服务器程序启动后,默认监听28080端口,当客户端连接时,会根据URL参数来将客户端连接加入到某个房间,如果房间号参数为空,则代表该客户端期望创建一个房间。先创建房间并加入的客户端作为answer端,等待offer端的连接。当从某个客户端连接收到消息后,会广播给房间内的其他客户端。当客户端断开连接时,便将其从房间中移除。
当然这仅是一个演示版程序,并未对历史建立的房间进行回收,同时也没有进行身份认证等安全方面的控制。
接下来,我们再来看看借助信令服务器进行端到端实时通信的端侧WebRTC应用的实现。
WebRTC应用的代码通常都很“样板化”。在开发WebRTC应用程序时,信令连接、设置本地和远程描述、收集ICE候选以及转发信令消息等步骤都是一些常见且重复性较高的任务。这些步骤在不同的WebRTC应用程序中通常都大同小异。以下是这些重复性任务的一些具体步骤示例:
1) 信令连接处理
– 创建信令通道(如WebSocket连接)
– 监听连接建立、断开等事件
– 通过信令通道交换offer/answer等信令消息
2) 本地和远程描述设置
– 创建c实例
– 设置本地描述(createOffer/createAnswer)
– 设置远程描述(setRemoteDescription)
3) ICE 候选收集与交换
– 监听ICE候选事件,收集本地ICE候选
– 通过信令通道交换ICE候选信息
– 将远程ICE候选添加到RTCPeerConnection实例
4) 信令消息转发
– 接收来自远程的信令消息
– 根据消息类型,转发给本地RTCPeerConnection实例
这些基本步骤在大多数WebRTC应用程序中都是必需的。我们的示例代码也不例外,下面就是webrtc-peer程序源码,有些长,也很繁琐:
// webrtc-first-lesson/part2/webrtc-peer/main.go
package main
import (
"encoding/json"
"flag"
"fmt"
"log"
"time"
"github.com/gorilla/websocket"
"github.com/pion/logging"
"github.com/pion/webrtc/v3"
)
type signalMsg struct {
Type string `json:"type"`
Data string `json:"data"`
}
var (
signalingServer string
roomID string
)
func init() {
flag.StringVar(&signalingServer, "server", "ws://localhost:28080/ws", "Signaling server WebSocket URL")
flag.StringVar(&roomID, "room", "", "Room ID (leave empty to create a new room)")
flag.Parse()
}
func main() {
// Connect to signaling server
signalingURL := fmt.Sprintf("%s?room=%s", signalingServer, roomID)
conn, _, err := websocket.DefaultDialer.Dial(signalingURL, nil)
if err != nil {
log.Fatal("Error connecting to signaling server:", err)
}
defer conn.Close()
log.Println("connect to signaling server ok")
// Create a new RTCPeerConnection
config := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{
URLs: []string{"stun:stun.l.google.com:19302"},
},
},
}
// 创建一个自定义的日志工厂
loggerFactory := logging.NewDefaultLoggerFactory()
loggerFactory.DefaultLogLevel = logging.LogLevelTrace
//loggerFactory.DefaultLogLevel = logging.LogLevelInfo
//loggerFactory.DefaultLogLevel = logging.LogLevelDebug
// Enable detailed logging
s := webrtc.SettingEngine{}
s.LoggerFactory = loggerFactory
s.SetICETimeouts(5*time.Second, 5*time.Second, 5*time.Second)
api := webrtc.NewAPI(webrtc.WithSettingEngine(s))
peerConnection, err := api.NewPeerConnection(config)
if err != nil {
log.Fatal(err)
}
// Create a datachannel
dataChannel, err := peerConnection.CreateDataChannel("test", nil)
if err != nil {
log.Fatal(err)
}
dataChannel.OnOpen(func() {
log.Println("Data channel is open")
go func() {
for {
err := dataChannel.SendText("Hello from " + roomID)
if err != nil {
log.Println(err)
}
time.Sleep(5 * time.Second)
}
}()
})
dataChannel.OnMessage(func(msg webrtc.DataChannelMessage) {
log.Printf("Received message: %s\n", string(msg.Data))
})
// Set the handler for ICE connection state
peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) {
log.Printf("ICE Connection State has changed: %s\n", connectionState.String())
})
// Set the handler for Peer connection state
peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) {
log.Printf("Peer Connection State has changed: %s\n", s.String())
})
// Set the handler for Signaling state
peerConnection.OnSignalingStateChange(func(s webrtc.SignalingState) {
log.Printf("Signaling State has changed: %s\n", s.String())
})
// Register data channel creation handling
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
log.Printf("New DataChannel %s %d\n", d.Label(), d.ID())
d.OnOpen(func() {
log.Printf("Data channel '%s'-'%d' open.\n", d.Label(), d.ID())
})
d.OnMessage(func(msg webrtc.DataChannelMessage) {
log.Printf("Message from DataChannel '%s': '%s'\n", d.Label(), string(msg.Data))
})
})
// Set the handler for ICE candidate generation
peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) {
if i == nil {
return
}
candidateString, err := json.Marshal(i.ToJSON())
if err != nil {
log.Println(err)
return
}
if writeErr := conn.WriteJSON(&signalMsg{
Type: "candidate",
Data: string(candidateString),
}); writeErr != nil {
log.Println(writeErr)
}
})
// Handle incoming messages from signaling server
go func() {
for {
_, rawMsg, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message:", err)
return
}
log.Println("recv msg from signaling server")
var msg signalMsg
if err := json.Unmarshal(rawMsg, &msg); err != nil {
log.Println("Error parsing message:", err)
continue
}
log.Println("recv msg is", msg)
switch msg.Type {
case "offer":
log.Println("recv a offer msg")
offer := webrtc.SessionDescription{}
if err := json.Unmarshal([]byte(msg.Data), &offer); err != nil {
log.Println("Error parsing offer:", err)
continue
}
if err := peerConnection.SetRemoteDescription(offer); err != nil {
log.Println("Error setting remote description:", err)
continue
}
answer, err := peerConnection.CreateAnswer(nil)
if err != nil {
log.Println("Error creating answer:", err)
continue
}
if err := peerConnection.SetLocalDescription(answer); err != nil {
log.Println("Error setting local description:", err)
continue
}
answerString, err := json.Marshal(answer)
if err != nil {
log.Println("Error encoding answer:", err)
continue
}
if err := conn.WriteJSON(&signalMsg{
Type: "answer",
Data: string(answerString),
}); err != nil {
log.Println("Error sending answer:", err)
}
log.Println("send answer ok")
case "answer":
log.Println("recv a answer msg")
answer := webrtc.SessionDescription{}
if err := json.Unmarshal([]byte(msg.Data), &answer); err != nil {
log.Println("Error parsing answer:", err)
continue
}
if err := peerConnection.SetRemoteDescription(answer); err != nil {
log.Println("Error setting remote description:", err)
}
log.Println("set remote desc for answer ok")
case "candidate":
candidate := webrtc.ICECandidateInit{}
if err := json.Unmarshal([]byte(msg.Data), &candidate); err != nil {
log.Println("Error parsing candidate:", err)
continue
}
if err := peerConnection.AddICECandidate(candidate); err != nil {
log.Println("Error adding ICE candidate:", err)
}
log.Println("adding ICE candidate:", candidate)
}
}
}()
// Create an offer if we are the peer to join the room
if roomID != "" {
offer, err := peerConnection.CreateOffer(nil)
if err != nil {
log.Fatal(err)
}
if err := peerConnection.SetLocalDescription(offer); err != nil {
log.Fatal(err)
}
offerString, err := json.Marshal(offer)
if err != nil {
log.Fatal(err)
}
if err := conn.WriteJSON(&signalMsg{
Type: "offer",
Data: string(offerString),
}); err != nil {
log.Fatal(err)
}
log.Printf("send offer to signaling server ok\n")
}
// Wait forever
select {}
}
通过代码,我们看到:这个使用Go实现的WebRTC对等连接示例程序通过WebSocket与信令服务器通信,创建和管理RTCPeerConnection,处理ICE候选、offer和answer,并实现了数据通道功能。程序支持创建新房间或加入现有房间,展示了完整的WebRTC连接建立流程,包括信令交换和ICE处理。它通过对pion/webrtc的日志级别设置让其具有详细的日志记录能力,这为我们后续通过日志分别WebRTC建连各个阶段奠定了基础。
下面是实验环境的拓扑图:
webrtc-peer分别位于两台服务器上,其中Host A是一台位于NAT后面的内网主机,而HOST B则是一台位于美国的公网主机,信令服务器搭建在HOST B上,stun服务器使用的是Google提供的公网免费stun server。
下面是信令服务器和两端peer服务器的编译和启动步骤:
我们先启动信令服务器:
//在Host B上signaling-server目录下
$make
$./signaling-server
2024/08/20 21:45:50 Signaling server starting on :28080
接下来,启动Host A上的webrtc-peer程序:
//在Host A上webrtc-peer目录下
$make
$./webrtc-peer -server ws://206.189.166.16:28080/ws
这时信令服务器就会发现有新的websocket连入,并创建了room_6(这只是多次运行中的某一次的room id罢了):
2024/08/20 21:48:52 New WebSocket connection from: 47.93.3.95:17355
2024/08/20 21:48:52 Created new room: room_6
2024/08/20 21:48:52 Client[47.93.3.95:17355] joined room room_6
然后我们启动Host B上的webrtc-peer程序,将这一端加入到上面创建的room_6中:
//在Host B上webrtc-peer目录下
$make
$./webrtc-peer -room room_6 -server ws://206.189.166.16:28080/ws
这之后,信令服务器也会发现Host B上的webrtc-peer的连接。之后便开始从信令交互开始逐步实现端到端的建连。以下是对各个阶段产生的详细日志的分析:
"2024/08/20 21:45:48 connect to signaling server ok"
以上日志表示成功连接到信令服务器。如果房间号为空,则该peer(answer)先启动并在信令服务器建立房间,然后另一个peer(offer)加入该房间,通过信令服务器交换信息。
下面日志则是表示接收到另一个peer的offer SDP:
"2024/08/20 21:45:55 recv msg is {offer {"type":"offer","sdp":"v=0\r\no=- 2149168073199454578 1724143555 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\na=msid-semantic:WMS*\r\na=fingerprint:sha-256 A6:D6:AE:F3:30:0D:D8:07:D2:23:C9:A5:69:27:F2:CC:B1:8C:A4:DB:30:79:E7:62:9B:09:87:B7:68:1F:55:A7\r\na=extmap-allow-mixed\r\na=group:BUNDLE 0\r\nm=application 9 UDP/DTLS/SCTP webrtc-datachannel\r\nc=IN IP4 0.0.0.0\r\na=setup:actpass\r\na=mid:0\r\na=sendrecv\r\na=sctp-port:5000\r\na=ice-ufrag:TYfjBFmqpgGEtKbh\r\na=ice-pwd:NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB\r\n"}}
其中”recv a offer msg”表示程序识别到收到了offer消息。而”offer := webrtc.SessionDescription{}”及后续代码则是处理offer,创建answer并发送回给另一个peer。
在WebRTC中,信令服务器用于交换SDP(Session Description Protocol)信息,SDP描述了连接的媒体信息,如编解码器、IP 地址、端口等。先启动的peer创建房间,等待offer,后加入的peer发送offer后,等待answer的回复,双方通过信令服务器交换这些信息以建立连接。
接下来,便是两端的ICE流程。
下面一行日志表示开始收集ICE 候选者,这里是一个host类型的候选者:
"2024/08/20 21:45:55 adding ICE candidate: {candidate:3384150427 1 udp 2130706431 206.189.166.16 52256 typ host 0xc000210230 0xc0002121fe <nil>}"
后续有多个类似的日志,分别添加不同类型的候选者,如 host、srflx(Server Reflexive)等:
2024/08/20 21:45:55 adding ICE candidate: {candidate:604015337 1 udp 2130706431 10.46.0.5 38367 typ host 0xc000210260 0xc000212250 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:3019421960 1 udp 2130706431 2604:a880:2:d0::2094:3001 48394 typ host 0xc000210290 0xc000212298 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2090009598 1 udp 2130706431 10.0.0.1 58895 typ host 0xc0002102d0 0xc0002122e0 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:233762139 1 udp 2130706431 172.17.0.1 58343 typ host 0xc000210300 0xc000212328 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2943811937 1 udp 1694498815 2604:a880:2:d0::2094:3001 40480 typ srflx raddr :: rport 40480 0xc00038c070 0xc00038e050 <nil>}
2024/08/20 21:45:55 adding ICE candidate: {candidate:2614874796 1 udp 1694498815 206.189.166.16 38534 typ srflx raddr 0.0.0.0 rport 38534 0xc000210760 0xc000212b98 <nil>}
不过,在输出的日志中,我们看到并没有明确输出我们期待的经过 Candidate Priorization(候选者优先级排序)后的候选者排序列表。
注:重温一下ICE(Interactive Connectivity Establishment),这是一种用于在两个peer之间建立连接的协议,通过收集各种类型的候选者(如 host 表示本机地址、srflx 表示通过 NAT 反射得到的地址等),增加连接成功的可能性。
在ICE连接中,会确定一个controlling方和一个controlled方,用于决定连接的发起和响应顺序。 下面这行输出日志表示本端不是controlling方:
"ice DEBUG: 21:45:55.401065 agent.go:395: Started agent: isControlling? false, remoteUfrag: "TYfjBFmqpgGEtKbh", remotePwd: "NGdAyXsOgVwFfzXnlLmNrcWrBgJWFceB"
这个阶段日志中没有明确输出检查列表,但日志中有大量的“Ping STUN from… to…”表示正在进行连接检查,这些日志汇总在一起可以看成是形成的检查列表。例如:
ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256。
每一端都会通过发送STUN请求来检查不同候选者之间的连接性。
日志中有很多类似的日志表示收到了来自特定候选者的成功响应:
"ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115"
根据连接检查的结果,如果发现Peer Reflexive 候选,也会有相应的日志输出,比如:
ice DEBUG: 21:45:25.771665 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61194
ice DEBUG: 21:45:25.772355 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:26408
ice DEBUG: 21:45:25.775320 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:40491
ice DEBUG: 21:45:25.776894 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:5767
ice DEBUG: 21:45:25.777018 agent.go:1147: Adding a new peer-reflexive candidate: 192.168.0.124:61432
... ...
日志中大量的”Ping STUN”和”Inbound STUN (SuccessResponse)”表示正在进行 NAT 穿透尝试。例如:
ice TRACE: 21:45:55.401676 agent.go:999: Ping STUN from udp4 host 172.17.0.1:7115 to udp4 host 206.189.166.16:52256
ice TRACE: 21:45:55.563530 selection.go:229: Inbound STUN (SuccessResponse) from udp4 host 206.189.166.16:52256 to udp4 host 172.17.0.1:7115
通过STUN请求和响应来确定是否能够穿透NAT,如果穿透失败,则将其标记为failed:
ice TRACE: 21:45:56.274839 agent.go:550: Maximum requests reached for pair prio 9151314440652587007 (local, prio 2130706431) udp4 host 172.18.0.1:59520 <-> udp4 host 10.0.0.1:58895 (remote, prio 2130706431), state: in-progress, nominated: false, nominateOnBindingSuccess: false, marking it as failed
如果能够成功穿透,则可以建立连接。下面的日志表示选出了最终的最佳候选者对:
ice TRACE: 21:45:56.656900 agent.go:524: Set selected candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false
ice TRACE: 21:45:56.823017 selection.go:239: Found valid candidate pair: prio 9151314440652587007 (local, prio 2130706431) udp4 host 192.168.10.1:60662 <-> udp4 host 206.189.166.16:52256 (remote, prio 2130706431), state: succeeded, nominated: true, nominateOnBindingSuccess: false
一旦确定了最佳候选者对,连接就算建立成功了!
接下来,就是打开datachannel通道并进行数据传输了!
下面日志表示数据通道已打开:
"Data channel is open"
下面日志表示创建了一个名为“test”的数据通道:
"New DataChannel test 824638605290"
下面日志表示数据通道打开成功:
"Data channel 'test'-'824638605290' open"
示例代码中,启动一个goroutine用于定时向data channel发送数据,当出现下面日志时,表示接收到来自另一个 peer 的数据:
"Message from DataChannel 'test': 'Hello from room_6'"
在这篇文章中,我通过使用Go语言开源项目pion/webrtc实现的webrtc端侧应用,为大家详细展示了WebRTC应用的建连过程。
首先,我实现了一个基于WebSocket的简易信令服务器。这个信令服务器基于Room抽象模型,使用全局变量来管理房间和连接,并进行消息广播。
接下来,我介绍了端侧WebRTC应用的实现。这个应用通过与信令服务器通信,创建RTCPeerConnection,处理ICE候选、offer和answer,以及实现数据通道功能。我还通过设置TRACE日志级别,展示了详细的建连流程。
之后,我在实验环境的实际执行了上述程序,并通过对日志的分析展示了建连过程。这些分析涵盖了信令服务连接和SDP交互、ICE候选收集与优先级排序、ICE 连通性检查各子阶段、NAT穿透尝试及最佳候选者对确定,以及数据通道打开和数据传输。希望这样的分析可以帮助大家更深刻的理解和体会建连过程。
WebRTC网络结构和建连就先讲到这里,后面的系列文章中,我们会开始聚焦WebRTC技术栈的另外一个主要方面:音视频质量,包括编码器以及媒体流处理等。
本文涉及的Go源码在这里可以下载到 – https://github.com/bigwhite/experiments/blob/master/webrtc-first-lesson/part2
Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2024, bigwhite. 版权所有.
2024-12-23 23:02:42
本文永久链接 – https://tonybai.com/2024/12/23/convert-github-issue-to-markdown-with-issue2md
到2024年底,不论你是否承认,AI时代都已经到来!近两个月,三大顶级商业AI模型巨头:Claude Sonnet 3.5、Google Gemini 2.0 Flash Experimental以及ChatGPT o3你方唱罢我登场,好不热闹!
作为走在AI应用前沿的程序员,利用AI辅助自己提高学习和工作实践的效率都是必不可少的。在使用AI的过程中,我们经常需要向其提供一些文档资料,对于文字资料,AI更偏爱TXT、Markdown、PDF等格式的文件。部署在Vercel上的MarkdownDown支持输入网页URL并将其转换为Markdown,而微软开源的MarkItdown则能将多种格式(pdf、ppt、word、html、zip等)转换为Markdown。这些工具在实践中帮助我们实现对AI的快速“投喂”。
然而,一些资料,如GitHub Issues,尚不能通过上述工具方便地转换为干净的、无额外干扰内容的Markdown或其他适合投喂给AI的格式。受到MarkdownDown的启发,我思考是否可以将GitHub Issues转换为Markdown,最终促成了issue2md这个想法。该工具旨在简化GitHub Issues与Markdown之间的转换过程,使得开发者可以更高效地利用AI理解Github issue中的内容,包括用户讨论中的一些观点和想法。
三个月前,我利用AI完成了issue2md这个小工具,我自己甚至没有写下一行代码。我仅仅对其提出一个小小的要求,那就是不要依赖任何第三方包,仅可以依赖Go标准库。在这三个月中,该工具给了我很大的帮助,将由它生成的Github Issue对应的Markdown文档投喂给AI后,可以让我快速理解Github issue的要点,尤其是那些历经几年讨论,积累了数百条comment的issue!
这里我将issue2md放到github上供大家下载使用,也希望能给大家带去相同的帮助。
下面简单介绍一下issue2md的用法。
issue2md项目有两个工具,或者说两种使用模式,一种是命令行模式,使用issue2md这个命令行工具。另外一种则是Web模式,使用issue2mdweb这个工具。
如果你喜欢命令行模式,那么你只需要使用下面命令安装issue2md即可:
$go install github.com/bigwhite/issue2md/cmd/issue2md@latest
issue2md cli程序的使用方法非常简单:
Usage: issue2md issue-url [markdown-file]
Arguments:
issue-url The URL of the GitHub issue to convert.
markdown-file (optional) The output markdown file.
它的第一个参数是github issue的URL。以Go 1.24版本json包增加对omitzero的支持的issue为例,它的url是https://github.com/golang/go/issues/45669,我们原封不动的将其作为issue2md的第一个参数执行:
$issue2md https://github.com/golang/go/issues/45669
Issue and comments saved as Markdown in file golang_go_issue_45669.md
issue2md cli默认会生成一个命名格式如下的文件:
{owner}_{repo}_issue_number.md
其内容使用markdown编辑器打开并渲染后将呈现如下的效果:
当然你也可以通过传入第二个命令行参数,作为最终生成的markdown的文件名!
如果你不喜欢命令行模式,你可以使用issue2mdweb提供的Web模式。最简单的启动一个issue2mdweb服务的方法就是利用我发布到Docker hub上的issue2md的公共镜像,你可以像下面这样在本地或你的私有云里运行一个issue2mdweb服务:
$docker run -d -p 8080:8080 bigwhite/issue2mdweb
然后用你的浏览器打开http://{host}:8080这个地址,你将看到如下的页面:
在中间的文本框中输入你要转换的Github issue地址,比如前面的https://github.com/golang/go/issues/45669,点击“Convert”,你的浏览器就会自动将转换后的Markdown文件下载到你的本地,文件命名和issue2md cli的默认命名格式一致!
如果你不想使用Docker运行,你可以自行下载issue2md代码并编译,也可以使用scripts中的命令将issue2mdweb安装为一个Systemd unit服务!
这里要注意的是,issue2md使用了Go标准口实现了对Github API的访问且没有使用任何账号信息,它仅适合将Public仓库的issue转换为Markdown,并且由于Github对API调用的限速,你在使用issue2md时不能过于频繁!此外,你若发现issue2md的bug或者你有什么新的想法,欢迎在issue2md仓库中提出你宝贵的issue。
最后打个“广告”,根据极客时间的专栏推广计划,我在春节前会为“Go语言第一课”专栏续写五篇文章,其中的第一篇“Go测试的5个使用建议”已经上线。
无论你是“Go语言第一课”的学员,还是首次听说这门专栏的小伙伴,我都欢迎你阅读这些文章,希望这些专栏文章能你带去新的收获!也欢迎你将阅读后的感受在评论区分享出来!
Gopher部落知识星球在2024年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。同时,我们也会加强代码质量和最佳实践的分享,包括如何编写简洁、可读、可测试的Go代码。此外,我们还会加强星友之间的交流和互动。欢迎大家踊跃提问,分享心得,讨论技术。我会在第一时间进行解答和交流。我衷心希望Gopher部落可以成为大家学习、进步、交流的港湾。让我相聚在Gopher部落,享受coding的快乐! 欢迎大家踊跃加入!
著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。
Gopher Daily(Gopher每日新闻) – https://gopherdaily.tonybai.com
我的联系方式:
商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。
© 2024, bigwhite. 版权所有.