标签 Package 下的文章

Go导出标识符:那些鲜为人知的细节

本文永久链接 – 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导出标识符的细节可能是鲜少人探讨的,在这篇博文中,我们将系统地了解这些机制,希望能为各位小伙伴带来更深入的理解。

1. Go对导出标识符的定义

我们先回顾一下Go语言规范(go spec)对导出标识符的定义

我们通常使用英文字母来命名标识符,因此可以将上述定义中的第一句理解为:以大写英文字母开头的标识符即为导出标识符。

注:Unicode字符类别Lu(Uppercase Letter)包含所有的大写字母。这一类别不仅包括英文大写字母,还涵盖多种语言的大写字符,例如希腊字母、阿拉伯字母、希伯来字母和西里尔字母等。然而,我非常不建议大家使用非英文大写字母来表示导出标识符,因为这可能会挑战大家的认知习惯。

而第二句后半部分的描述往往被我们忽视或理解不够到位。一个类型的字段名和方法名可以是导出的,但并没有明确要求其关联的类型本身也必须是导出的

这为我们提供了进一步探索Go导出标识符细节的机会。接下来,我们就用具体示例看看是否可以在包外访问非导出类型的导出字段以及导出方法。

2. 在包外访问非导出类型的导出字段

我们首先定义一个带有导出字段的非导出类型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这个非导出类型。

采用类似的方案,我们接下来再看看是否可以在包外访问非导出类型的导出方法。

3. 在包外访问非导出类型的导出方法

我们为非导出类型添加两个导出方法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

我们看到,无论是通过非导出类型的指针,还是通过非导出类型的变量复本都可以成功调用非导出类型的导出方法。

提及方法,我们会顺带想到接口,非导出类型是否可以实现某个外部包定义的接口呢?我们继续往下看。

4. 非导出类型实现某个外部包的接口

在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。

注:关于一个类型的方法集合的判定规则,可以参考我的极客时间《Go语言第一课》专栏的第25讲

接下来,我们再来考虑一个场景,即非导出类型用作嵌入字段的情况,我们要看看该非导出类型的导出方法和导出字段是否会promote到外部类型中。

5. 非导出类型用作嵌入字段

我们改造一下示例,新版的带有嵌入字段的结构见下面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增加了泛型支持,那么非导出类型是否可以用作泛型函数和泛型类型的类型实参呢?最后我们来看看这个细节。

6. 非导出类型用作泛型函数和泛型类型的类型实参

和前面一样,我们先定义用于该示例的带有导出字段和导出方法的非导出类型:

// 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

7. 非导出类型使用导出字段以及导出方法的用途

前面的诸多示例证明了:即使类型本身是非导出的,但其内部的导出字段以及它的导出方法依然可以在外部包中使用,并且在实现接口、嵌入字段、泛型等使用场景下均有效。

到这里,你可能会提出这样一个问题:会有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,使库的接口更加简洁。

  • 控制实例的创建和管理

通过非导出类型,开发者还可以确保外部代码无法直接实例化类型,而必须通过导出的构造函数或工厂函数,就像前面举的示例那样。这种模式可以保证对象始终以特定的方式初始化,避免错误使用。同时,它还可以用来实现更复杂的初始化逻辑,如依赖注入或资源管理。

  • 在接口实现中的作用

非导出类型可以用来实现导出的接口,从而将接口的实现细节完全隐藏。对于用户来说,只需要关心接口的定义,而无需关注其实现。

8. 小结

本文探讨了Go语言中的导出标识符及其相关细节,特别是非导出类型如何与其导出字段和导出方法结合使用。

尽管某些类型是非导出的,其内部的导出字段和方法依然可以在包外访问。此外,非导出类型在实现接口、嵌入字段和泛型中也展现出良好的应用。这种设计不仅促进了封装和接口实现的灵活性,还允许开发者通过构造函数返回非导出类型的实例,从而有效控制实例的创建与管理。这种方式帮助隐藏实现细节,简化外部接口,使得代码结构更加清晰。

本文涉及的源码可以在这里下载。


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“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 gcflags的使用模式与完整参数选项列表

本文永久链接 – 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的使用模式,尤其是其中的package pattern应该如何正确使用?

如果你能正确回答上述两个问题,那你就基本掌握了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'

其中:

  • [pattern=](可选):包模式(package pattern),用于作用范围控制,即限定参数仅应用于特定的包。如果省略此部分,则参数仅适用于命令行中指定的包。
  • arg list:参数选项列表,多个参数以空格分隔。

对包模式有很好地理解并非是使用好gcflags的必要条件。但在一些复杂项目中,我们可能会通过包模式精确控制调试和优化,在这种情况下,对包模式有深入理解还是大有裨益的。

包模式是一种通过匹配规则指定目标包的方式,常见的包模式有几下几种:

  • ./…:匹配当前目录及其所有子目录中的包。
  • /DIR/…:匹配/DIR及其子目录中的包。
  • cmd/…:匹配Go仓库中cmd目录下的所有命令包。
  • github.com/user/repo/…:匹配该github仓库中的所有包。
  • all:GOPATH模式下,匹配的是所有GOPATH路径中的包,Go module模式下,all匹配主模块及其所有依赖的包(包括测试依赖)。
  • std:仅匹配标准库包。
  • cmd:匹配Go仓库中的Go命令及其内部包(internal)。

基于上述关于gcflags使用形式以及包模式的说明,我们举几个示例来直观理解一下gcflags的用法:

  • 对单个包设置参数
$go build -gcflags=-S fmt

上述命令中的参数-S仅作用于fmt包,显示其反汇编代码。

  • 对特定模式(比如all/std等)的包设置参数
$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(以及-ldflags)是Go构建工具中的重要选项,能极大提升调试和优化效率。
  • gcflags的完整的参数选项需通过底层工具获取,即go tool compile -h和go tool link -h。
  • 对包模式的灵活使用能够精确控制gcflags参数的作用范围,为复杂项目提供了更大的自由度。

通过本篇文章,希望你能掌握查看gcflags完整参数列表的方法以及gcflags的使用模式,并在构建和调试Go项目时能更加得心应手。


Gopher部落知识星球在2025年将继续致力于打造一个高品质的Go语言学习和交流平台。我们将继续提供优质的Go技术文章首发和阅读体验。并且,2025年将在星球首发“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