标签 编译器 下的文章

Go编译的几个细节,连专家也要停下来想想

本文永久链接 – https://tonybai.com/2024/11/11/some-details-about-go-compilation

在Go开发中,编译相关的问题看似简单,但实则蕴含许多细节。有时,即使是Go专家也需要停下来,花时间思考答案或亲自验证。本文将通过几个具体问题,和大家一起探讨Go编译过程中的一些你可能之前未曾关注的细节。

注:本文示例使用的环境为Go 1.23.0、Linux Kernel 3.10.0和CentOS 7.9。

1. Go编译默认采用静态链接还是动态链接?

我们来看第一个问题:Go编译默认采用静态链接还是动态链接呢?

很多人脱口而出:动态链接,因为CGO_ENABLED默认值为1,即开启Cgo。也有些人会说:“其实Go编译器默认是静态链接的,只有在使用C语言库时才会动态链接”。那么到底哪个是正确的呢?

我们来看一个具体的示例。但在这之前,我们要承认一个事实,那就是CGO_ENABLED默认值为1,你可以通过下面命令来验证这一点:

$go env|grep CGO_ENABLED
CGO_ENABLED='1'

验证Go默认究竟是哪种链接,我们写一个hello, world的Go程序即可:

// go-compilation/main.go

package main

import "fmt"

func main() {
    fmt.Println("hello, world")
}

构建该程序:

$go build -o helloworld-default main.go

之后,我们查看一下生成的可执行文件helloworld-default的文件属性:

$file helloworld-default
helloworld-default: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-default
   不是动态可执行文件

我们看到,虽然CGO_ENABLED=1,但默认情况下,Go构建出的helloworld程序是静态链接的(statically linked)。

那么默认情况下,Go编译器是否都会采用静态链接的方式来构建Go程序呢?我们给上面的main.go添加一行代码:

// go-compilation/main-with-os-user.go

package main

import (
    "fmt"
    _ "os/user"
)

func main() {
    fmt.Println("hello, world")
}

和之前的hello, world不同的是,这段代码多了一行包的空导入,导入的是os/user这个包。

编译这段代码,我们得到helloworld-with-os-user可执行文件。

$go build -o helloworld-with-os-user main-with-os-user.go

使用file和ldd检视文件helloworld-with-os-user:

$file helloworld-with-os-user
helloworld-with-os-user: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-os-user
    linux-vdso.so.1 =>  (0x00007ffcb8fd4000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fb5d6fce000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fb5d6c00000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb5d71ea000)

我们看到:一行新代码居然让helloworld从静态链接变为了动态链接,同时这也是如何编译出一个hello world版的动态链接Go程序的答案。

通过nm命令我们还可以查看Go程序依赖了哪些C库的符号:

$nm -a helloworld-with-os-user |grep " U "
                 U abort
                 U __errno_location
                 U fprintf
                 U fputc
                 U free
                 U fwrite
                 U malloc
                 U mmap
                 U munmap
                 U nanosleep
                 U pthread_attr_destroy
                 U pthread_attr_getstack
                 U pthread_attr_getstacksize
                 U pthread_attr_init
                 U pthread_cond_broadcast
                 U pthread_cond_wait
                 U pthread_create
                 U pthread_detach
                 U pthread_getattr_np
                 U pthread_key_create
                 U pthread_mutex_lock
                 U pthread_mutex_unlock
                 U pthread_self
                 U pthread_setspecific
                 U pthread_sigmask
                 U setenv
                 U sigaction
                 U sigaddset
                 U sigemptyset
                 U sigfillset
                 U sigismember
                 U stderr
                 U strerror
                 U unsetenv
                 U vfprintf

由此,我们可以得到一个结论,在默认情况下(CGO_ENABLED=1),Go会尽力使用静态链接的方式,但在某些情况下,会采用动态链接。那么究竟在哪些情况下会默认生成动态链接的程序呢?我们继续往下看。

2. 在何种情况下默认会生成动态链接的Go程序?

在以下几种情况下,Go编译器会默认(CGO_ENABLED=1)生成动态链接的可执行文件,我们逐一来看一下。

2.1 一些使用C实现的标准库包

根据上述示例,我们可以看到,在某些情况下,即使只依赖标准库,Go 仍会在CGO_ENABLED=1的情况下采用动态链接。这是因为代码依赖的标准库包使用了C版本的实现。虽然这种情况并不常见,但os/user包net包是两个典型的例子。

os/user包的示例在前面我们已经见识过了。user包允许开发者通过名称或ID查找用户账户。对于大多数Unix系统(包括linux),该包内部有两种版本的实现,用于解析用户和组ID到名称,并列出附加组ID。一种是用纯Go编写,解析/etc/passwd和/etc/group文件。另一种是基于cgo的,依赖于标准C库(libc)中的例程,如getpwuid_r、getgrnam_r和getgrouplist。当cgo可用(CGO_ENABLED=1),并且特定平台的libc实现了所需的例程时,将使用基于cgo的(libc支持的)代码,即采用动态链接方式。

同样,net包在名称解析(Name Resolution,即域名或主机名对应IP查找)上针对大多数Unix系统也有两个版本的实现:一个是纯Go版本,另一个是基于C的版本。C版本会在cgo可用且特定平台实现了相关C函数(比如getaddrinfo和getnameinfo等)时使用。

下面是一个简单的使用net包并采用动态链接的示例:

// go-compilation/main-with-net.go

package main

import (
    "fmt"
    _ "net"
)

func main() {
    fmt.Println("hello, world")
}

编译后,我们查看一下文件属性:

$go build -o helloworld-with-net main-with-net.go 

$file helloworld-with-net
helloworld-with-net: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped

$ldd helloworld-with-net
    linux-vdso.so.1 =>  (0x00007ffd75dfd000)
    libresolv.so.2 => /lib64/libresolv.so.2 (0x00007fdda2cf9000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007fdda2add000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fdda270f000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdda2f13000)

我们看到C版本实现依赖了libresolv.so这个用于名称解析的C库。

由此可得,当Go在默认cgo开启时,一旦依赖了标准库中拥有C版本实现的包,比如os/user、net等,Go编译器会采用动态链接的方式编译Go可执行程序。

2.2 显式使用cgo调用外部C程序

如果使用cgo与外部C代码交互,那么生成的可执行文件必然会包含动态链接。下面我们来看一个调用cgo的简单示例。

首先,建立一个简单的C lib:

// go-compilation/my-c-lib

$tree my-c-lib
my-c-lib
├── Makefile
├── mylib.c
└── mylib.h

// go-compilation/my-c-lib/Makefile

.PHONY:  all static

all:
        gcc -c -fPIC -o mylib.o mylib.c
        gcc -shared -o libmylib.so mylib.o
static:
        gcc -c -fPIC -o mylib.o mylib.c
        ar rcs libmylib.a mylib.o

// go-compilation/my-c-lib/mylib.h

#ifndef MYLIB_H
#define MYLIB_H

void hello();
int add(int a, int b);

#endif // MYLIB_H

// go-compilation/my-c-lib/mylib.c

#include <stdio.h>

void hello() {
    printf("Hello from C!\n");
}

int add(int a, int b) {
    return a + b;
}

执行make all构建出动态链接库libmylib.so!接下来,我们编写一个Go程序通过cgo调用libmylib.so中:

// go-compilation/main-with-call-myclib.go 

package main

/*
#cgo CFLAGS: -I ./my-c-lib
#cgo LDFLAGS: -L ./my-c-lib -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"

func main() {
    // 调用 C 函数
    C.hello()

    // 调用 C 中的加法函数
    result := C.add(3, 4)
    fmt.Printf("Result of addition: %d\n", result)
}

编译该源码:

$go build -o helloworld-with-call-myclib main-with-call-myclib.go

通过ldd可以看到,可执行文件helloworld-with-call-myclib是动态链接的,并依赖libmylib.so:

$ldd helloworld-with-call-myclib
    linux-vdso.so.1 =>  (0x00007ffcc39d8000)
    libmylib.so => not found
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f7166df5000)
    libc.so.6 => /lib64/libc.so.6 (0x00007f7166a27000)
    /lib64/ld-linux-x86-64.so.2 (0x00007f7167011000)

设置LD_LIBRARY_PATH(为了让程序找到libmylib.so)并运行可执行文件helloworld-with-call-myclib:

$ LD_LIBRARY_PATH=./my-c-lib:$LD_LIBRARY_PATH ./helloworld-with-call-myclib
Hello from C!
Result of addition: 7

2.3 使用了依赖cgo的第三方包

在日常开发中,我们经常依赖一些第三方包,有些时候这些第三方包依赖cgo,比如mattn/go-sqlite3。下面就是一个依赖go-sqlite3包的示例:

// go-compilation/go-sqlite3/main.go
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    // 打开数据库(如果不存在,则创建)
    db, err := sql.Open("sqlite3", "./test.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    // 创建表
    sqlStmt := `CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);`
    _, err = db.Exec(sqlStmt)
    if err != nil {
        log.Fatalf("%q: %s\n", err, sqlStmt)
    }

    // 插入数据
    _, err = db.Exec(`INSERT INTO user (name) VALUES (?)`, "Alice")
    if err != nil {
        log.Fatal(err)
    }

    // 查询数据
    rows, err := db.Query(`SELECT id, name FROM user;`)
    if err != nil {
        log.Fatal(err)
    }
    defer rows.Close()

    for rows.Next() {
        var id int
        var name string
        err = rows.Scan(&id, &name)
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%d: %s\n", id, name)
    }

    // 检查查询中的错误
    if err = rows.Err(); err != nil {
        log.Fatal(err)
    }
}

编译和运行该源码:

$go build demo
$ldd demo
    linux-vdso.so.1 =>  (0x00007ffe23d8e000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007faf0ddef000)
    libpthread.so.0 => /lib64/libpthread.so.0 (0x00007faf0dbd3000)
    libc.so.6 => /lib64/libc.so.6 (0x00007faf0d805000)
    /lib64/ld-linux-x86-64.so.2 (0x00007faf0dff3000)
$./demo
1: Alice

到这里,有些读者可能会问一个问题:如果需要在上述依赖场景中生成静态链接的Go程序,该怎么做呢?接下来,我们就来看看这个问题的解决细节。

3. 如何在上述情况下实现静态链接?

到这里是不是有些烧脑了啊!我们针对上一节的三种情况,分别对应来看一下静态编译的方案。

3.1 仅依赖标准包

在前面我们说过,之所以在使用os/user、net包时会在默认情况下采用动态链接,是因为Go使用了这两个包对应功能的C版实现,如果要做静态编译,让Go编译器选择它们的纯Go版实现即可。那我们仅需要关闭CGO即可,以依赖标准库os/user为例:

$CGO_ENABLED=0 go build -o helloworld-with-os-user-static main-with-os-user.go
$file helloworld-with-os-user-static
helloworld-with-os-user-static: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ldd helloworld-with-os-user-static
    不是动态可执行文件

3.2 使用cgo调用外部c程序(静态链接)

对于依赖cgo调用外部c的程序,我们要使用静态链接就必须要求外部c库提供静态库,因此,我们需要my-c-lib提供一份libmylib.a,这通过下面命令可以实现(或执行make static):

$gcc -c -fPIC -o mylib.o mylib.c
$ar rcs libmylib.a mylib.o

有了libmylib.a后,我们还要让Go程序静态链接该.a文件,于是我们需要修改一下Go源码中cgo链接的flag,加上静态链接的选项:

// go-compilation/main-with-call-myclib-static.go
... ...
#cgo LDFLAGS: -static -L my-c-lib -lmylib
... ...

编译链接并查看一下文件属性:

$go build -o helloworld-with-call-myclib-static main-with-call-myclib-static.go

$file helloworld-with-call-myclib-static
helloworld-with-call-myclib-static: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=b3da3ed817d0d04230460069b048cab5f5bfc3b9, not stripped

我们得到了预期的结果!

3.3 依赖使用cgo的外部go包(静态链接)

最麻烦的是这类情况,要想实现静态链接,我们需要找出外部go依赖的所有c库的.a文件(静态共享库)。以我们的go-sqlite3示例为例,go-sqlite3是sqlite库的go binding,它依赖sqlite库,同时所有第三方c库都依赖libc,我们还要准备一份libc的.a文件,下面我们就先安装这些:

$yum install -y gcc glibc-static sqlite-devel
... ...

已安装:
  sqlite-devel.x86_64 0:3.7.17-8.el7_7.1                                                                                          

更新完毕:
  glibc-static.x86_64 0:2.17-326.el7_9.3

接下来,我们就来以静态链接的方式在go-compilation/go-sqlite3-static下编译一下:

$go build -tags 'sqlite_omit_load_extension' -ldflags '-linkmode external -extldflags "-static"' demo

$file ./demo
./demo: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=c779f5c3eaa945d916de059b56d94c23974ce61c, not stripped

这里命令行中的-tags ‘sqlite_omit_load_extension’用于禁用SQLite3的动态加载功能,确保更好的静态链接兼容性。而-ldflags ‘-linkmode external -extldflags “-static”‘的含义是使用外部链接器(比如gcc linker),并强制静态链接所有库。

我们再看完略烧脑的几个细节后,再来看一个略轻松的话题。

4. Go编译出的可执行文件过大,能优化吗?

Go编译出的二进制文件一般较大,一个简单的“Hello World”程序通常在2MB左右:

$ls -lh helloworld-default
-rwxr-xr-x 1 root root 2.1M 11月  3 10:39 helloworld-default

这一方面是因为Go将整个runtime都编译到可执行文件中了,另一方面也是因为Go静态编译所致。那么在默认情况下,Go二进制文件的大小还有优化空间么?方法不多,有两种可以尝试:

  • 去除符号表和调试信息

在编译时使用-ldflags=”-s -w”标志可以去除符号表和调试符号,其中-s用于去掉符号表和调试信息,-w用于去掉DWARF调试信息,这样能显著减小文件体积。以helloworld为例,可执行文件的size减少了近四成:

$go build -ldflags="-s -w" -o helloworld-default-nosym main.go
$ls -l
-rwxr-xr-x 1 root root 2124504 11月  3 10:39 helloworld-default
-rwxr-xr-x 1 root root 1384600 11月  3 13:34 helloworld-default-nosym
  • 使用tinygo

TinyGo是一个Go语言的编译器,它专为资源受限的环境而设计,例如微控制器、WebAssembly和其他嵌入式设备。TinyGo的目标是提供一个轻量级的、能在小型设备上运行的Go运行时,同时尽可能支持Go语言的特性。tinygo的一大优点就是生成的二进制文件通常比标准Go编译器生成的文件小得多:

$tinygo build -o helloworld-tinygo main.go
$ls -l
总用量 2728
-rwxr-xr-x  1 root root 2128909 11月  5 05:43 helloworld-default*
-rwxr-xr-x  1 root root  647600 11月  5 05:45 helloworld-tinygo*

我们看到:tinygo生成的可执行文件的size仅是原来的30%。

注:虽然TinyGo在特定场景(如IoT和嵌入式开发)中非常有用,但在常规服务器环境中,由于生态系统兼容性、性能、调试支持等方面的限制,可能并不是最佳选择。对于需要高并发、复杂功能和良好调试支持的应用,标准Go仍然是更合适的选择。

注:这里使用的tinygo为0.34.0版本。

5. 未使用的符号是否会被编译到Go二进制文件中?

到这里,相信读者心中也都会萦绕一些问题:到底哪些符号被编译到最终的Go二进制文件中了呢?未使用的符号是否会被编译到Go二进制文件中吗?在这一小节中,我们就来探索一下。

出于对Go的了解,我们已经知道无论是GOPATH时代,还是Go module时代,Go的编译单元始终是包(package),一个包(无论包中包含多少个Go源文件)都会作为一个编译单元被编译为一个目标文件(.a),然后Go链接器会将多个目标文件链接在一起生成可执行文件,因此如果一个包被依赖,那么它就会进入到Go二进制文件中,它内部的符号也会进入到Go二进制文件中。

那么问题来了!是否被依赖包中的所有符号都会被放到最终的可执行文件中呢?我们以最简单的helloworld-default为例,它依赖fmt包,并调用了fmt包的Println函数,我们看看Println这个符号是否会出现在最终的可执行文件中:

$nm -a helloworld-default | grep "Println"
000000000048eba0 T fmt.(*pp).doPrintln

居然没有!我们初步怀疑是inline优化在作祟。接下来,关闭优化再来试试:

$go build -o helloworld-default-noinline -gcflags='-l -N' main.go

$nm -a helloworld-default-noinline | grep "Println"
000000000048ec00 T fmt.(*pp).doPrintln
0000000000489ee0 T fmt.Println

看来的确如此!不过当使用”fmt.”去过滤helloworld-default-noinline的所有符号时,我们发现fmt包的一些常见的符号并未包含在其中,比如Printf、Fprintf、Scanf等。

这是因为Go编译器的一个重要特性:死码消除(dead code elimination),即编译器会将未使用的代码和数据从最终的二进制文件中剔除。

我们再来继续探讨一个衍生问题:如果Go源码使用空导入方式导入了一个包,那么这个包是否会被编译到Go二进制文件中呢?其实道理是一样的,如果用到了里面的符号,就会存在,否则不会。

以空导入os/user为例,即便在CGO_ENABLED=0的情况下,因为没有使用os/user中的任何符号,在最终的二进制文件中也不会包含user包:

$CGO_ENABLED=0 go build -o helloworld-with-os-user-noinline -gcflags='-l -N' main-with-os-user.go
[root@iZ2ze18rmx2avqb5xgb4omZ helloworld]# nm -a helloworld-with-os-user-noinline |grep user
0000000000551ac0 B runtime.userArenaState

但是如果是带有init函数的包,且init函数中调用了同包其他符号的情况呢?我们以expvar包为例看一下:

// go-compilation/main-with-expvar.go

package main

import (
    _ "expvar"
    "fmt"
)

func main() {
    fmt.Println("hello, world")
}

编译并查看一下其中的符号:

$go build -o helloworld-with-expvar-noinline -gcflags='-l -N' main-with-expvar.go
$nm -a helloworld-with-expvar-noinline|grep expvar
0000000000556480 T expvar.appendJSONQuote
00000000005562e0 T expvar.cmdline
00000000005561c0 T expvar.expvarHandler
00000000005568e0 T expvar.(*Func).String
0000000000555ee0 T expvar.Func.String
00000000005563a0 T expvar.init.0
00000000006e0560 D expvar..inittask
0000000000704550 d expvar..interfaceSwitch.0
... ...

除此之外,如果一个包即便没有init函数,但有需要初始化的全局变量,比如crypto包的hashes:

// $GOROOT/src/crypto/crypto.go
var hashes = make([]func() hash.Hash, maxHash)

crypto包的相关如何也会进入最终的可执行文件中,大家自己动手不妨试试。下面是我得到的一些输出:

$go build -o helloworld-with-crypto-noinline -gcflags='-l -N' main-with-crypto.go
$nm -a helloworld-with-crypto-noinline|grep crypto
00000000005517b0 B crypto.hashes
000000000048ee60 T crypto.init
0000000000547280 D crypto..inittask

有人会问:os/user包也有一些全局变量啊,为什么这些符号没有被包含在可执行文件中呢?比如:

// $GOROOT/src/os/user/user.go
var (
    userImplemented      = true
    groupImplemented     = true
    groupListImplemented = true
)

这就要涉及Go包初始化的逻辑了。我们看到crypto包包含在可执行文件中的符号中有crypto.init和crypto..inittask这两个符号,显然这不是crypto包代码中的符号,而是Go编译器为crypto包自动生成的init函数和inittask结构。

Go编译器会为每个包生成一个init函数,即使包中没有显式定义init函数,同时每个包都会有一个inittask结构,用于运行时的包初始化系统。当然这么说也不足够精确,如果一个包没有init函数、需要初始化的全局变量或其他需要运行时初始化的内容,则编译器不会为其生成init函数和inittask。比如上面的os/user包。

os/user包确实有上述全局变量的定义,但是这些变量是在编译期就可以确定值的常量布尔值,而且未被包外引用或在包内用于影响控制流。Go编译器足够智能,能够判断出这些初始化是”无副作用的”,不需要在运行时进行初始化。只有真正需要运行时初始化的包才会生成init和inittask。这也解释了为什么空导入os/user包时没有相关的init和inittask符号,而crypto、expvar包有的init.0和inittask符号。

6. 如何快速判断Go项目是否依赖cgo?

在使用开源Go项目时,我们经常会遇到项目文档中没有明确说明是否依赖Cgo的情况。这种情况下,如果我们需要在特定环境(比如CGO_ENABLED=0)下使用该项目,就需要事先判断项目是否依赖Cgo,有些时候还要快速地给出判断。

那究竟是否可以做到这种快速判断呢?我们先来看看一些常见的作法。

第一类作法是源码层面的静态分析。最直接的方式是检查源码中是否存在import “C”语句,这种引入方式是CGO使用的显著标志。

// 在项目根目录中执行
$grep -rn 'import "C"' .

这个命令会递归搜索当前目录下所有文件,显示包含import “C”的行号和文件路径,帮助快速定位CGO的使用位置。

此外,CGO项目通常包含特殊的编译指令,这些指令以注释形式出现在源码中,比如前面见识过的#cgo CFLAGS、#cgo LDFLAGS等,通过对这些编译指令的检测,同样可以来判断项目是否依赖CGO。

不过第一类作法并不能查找出Go项目的依赖包是否依赖cgo。而找出直接依赖或间接依赖是否依赖cgo,我们需要工具帮忙,比如使用Go工具链提供的命令分析项目依赖:

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'

其中ImportPath是依赖包的导入路径,而CgoFiles则是依赖中包含import “C”的Go源文件。我们以go-sqlite3那个依赖cgo的示例来验证一下:

// cd go-compilation/go-sqlite3

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}' ./...  | grep -v '\[\]'
runtime/cgo: [cgo.go]
github.com/mattn/go-sqlite3: [backup.go callback.go error.go sqlite3.go sqlite3_context.go sqlite3_load_extension.go sqlite3_opt_serialize.go sqlite3_opt_userauth_omit.go sqlite3_other.go sqlite3_type.go]

用空导入os/user的示例再来看一下:

$go list -deps -f '{{.ImportPath}}: {{.CgoFiles}}'  main-with-os-user.go | grep -v '\[\]'
runtime/cgo: [cgo.go]
os/user: [cgo_lookup_cgo.go getgrouplist_unix.go]

我们知道os/user有纯go和C版本两个实现,因此上述判断只能说“对了一半”,当我关闭CGO_ENABLED时,Go编译器不会使用基于cgo的C版实现。

那是否在禁用cgo的前提下对源码进行一次编译便能验证项目是否对cgo有依赖呢?这样做显然谈不上是一种“快速”的方法,那是否有效呢?我们来对上面的go-sqlite3项目做一个测试,我们在关闭CGO_ENABLED时,编译一下该示例:

// cd go-compilation/go-sqlite3
$ CGO_ENABLED=0 go build demo

我们看到,Go编译器并未报错!似乎该项目不需要cgo! 但真的是这样吗?我们运行一下编译后的demo可执行文件:

$ ./demo
2024/11/03 22:10:36 "Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub": CREATE TABLE IF NOT EXISTS user (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT);

我们看到成功编译出来的程序居然出现运行时错误,提示需要cgo!

到这里,没有一种方法可以快速、精确的给出项目是否依赖cgo的判断。也许判断Go项目是否依赖CGO并没有捷径,需要从源码分析、依赖检查和构建测试等多个维度进行。

7. 小结

在本文中,我们深入探讨了Go语言编译过程中的几个重要细节,尤其是在静态链接和动态链接的选择上。通过具体示例,我们了解到:

  • 默认链接方式:尽管CGO_ENABLED默认值为1,Go编译器在大多数情况下会采用静态链接,只有在依赖特定的C库或标准库包时,才会切换到动态链接。

  • 动态链接的条件:我们讨论了几种情况下Go会默认生成动态链接的可执行文件,包括依赖使用C实现的标准库包、显式使用cgo调用外部C程序,以及使用依赖cgo的第三方包。

  • 实现静态链接:对于需要动态链接的场景,我们也提供了将其转为静态链接的解决方案,包括关闭CGO、使用静态库,以及处理依赖cgo的外部包的静态链接问题。

  • 二进制文件优化:我们还介绍了如何通过去除符号表和使用TinyGo等方法来优化生成的Go二进制文件的大小,以满足不同场景下的需求。

  • 符号编译与死码消除:最后,我们探讨了未使用的符号是否会被编译到最终的二进制文件中,并解释了Go编译器的死码消除机制。

通过这些细节探讨,我希望能够帮助大家更好地理解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

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

致敬:程序员成长路上的良师与经典著作

本文永久链接 – https://tonybai.com/2024/09/10/programmer-mentors-and-their-classic-works

早上送孩子去幼儿园的路上,收到一个小伙伴的微信:

我这才意识到今天是教师节!为人师,自觉还不够格!但在这个特殊的日子,作为IT行业从业人员,我想向那些在计算机科学和编程领域给予我们启迪的“老师们”致敬。这些老师可能不是传统意义上站在讲台前的教育者,但他们通过自己的著作、思想和贡献,通过他们的智慧结晶,为我们指明了方向,为无数程序员的成长之路点亮了明灯。

这里我列举的作者与其著作也都是我个人从大学开始至今在计算机编程学习和实践之路上受到深刻影响的重要参考资料。这些书籍不仅丰富了我的知识,也激发了我对编程的热情和探索精神。每一位作者的独特视角和深入浅出的讲解,都让我在理解复杂概念时受益匪浅。希望也能引起大家的共鸣。

注:计算机领域巨匠甚多,笔者见识有限,不能一一列举,这里仅列出我亲自读过且对我影响深远的作者及其代表作品,至于像唐纳德·克努斯和他的巨著《计算机程序设计艺术》等,由于我并未拜读过,这里也就没有列出。

注:书中的图书封面图片可能并非该书最新版的封面,而是笔者购买时的版本的封面图片。

2. 编程语言

2.1 C语言/Go语言领域

2.1.1 Dennis Ritchie

大一的时候学校开设了C语言编程课,指定谭浩强老师的《C程序设计(第二版)》作为随课教材,当时我特意到大学书店花了银子买了本,并奉为皋臬。

直到我看到清华出版的影印版《C程序设计语言(第二版)》,才发现自己天真了,这本才是真正的“圣经”!

Dennis Ritchie,被誉为”C语言之父”,1983年图灵奖得主(与Ken Thompson同年获得)。他不仅创造了C语言,还与Ken Thompson一起开发了UNIX操作系统。刚刚过去的9月9日是其诞辰纪念日,MIT CSAIL在X上发文纪念了这位计算机先驱和现代编程语言奠基人:

他与Brian Kernighan合著的《The C Programming Language》被亲切地称为“K&R C”,是学习C语言的必读经典,书籍不厚,它以简洁明了的语言介绍了C语言的核心概念(遵循当时的ANSI C89/C90标准),影响了几代程序员。

2.1.2 Brian Kernighan

说完K&R中的R,我们再来说K。K指的是Brian Kernighan,他也是Bell实验室UNIX开发团队的重要成员,是C语言的主要推广者之一,他也是AWK语言中的最后的那个K。和Dennis Ritchie等动不动就是语言之父不同,Kernighan以写作风格闻名。他的写作风格清晰易懂,使复杂的概念变得平易近人,并以一种易于理解和应用的方式呈现给读者。这使得与Dennis Ritchie合著的《C程序设计语言》不仅是C语言语言特性的权威指南,更是编程语言类书籍技术写作的典范,之后很多编程语言类的书籍都参考Kernighan的风格,至少也会先从一个“Hello, World”开始全书的讲解。

其与P.J.Plauger合著的《The Elements of Programming Style》也是程序员眼中的经典。

2015年,已经70高龄的Kernighan又和Go团队的Alan Donovan合著了Go语言编程书籍领域公认的圣经《The Go Programming Language》。这本书与K&R C的风格很相似,作者们以清晰简洁的语言,系统且全面地介绍了Go的语法特性和编程理念,并通过大量的实例展示了Go在实际项目中的应用。书中不仅覆盖了基础知识,还深入探讨了并发编程、unsafe编程等Go高级主题。

2.2 C++

2.2.1 Bjarne Stroustrup

Bjarne Stroustrup是C++语言之父,他从1979年开始,在C语言的基础上添加了面向对象编程等特性,从而创造了C++这门强大而灵活的通用编程语言。C++经过ISO标准化后,他也是C++标准委员会的创始成员,并从那时起一直是一名活跃成员。如今,他还担任负责处理语言扩展提案的小组(进化工作组)的主席。

Bjarne Stroustrup的著作也是我入门和深入C++的必读经典,其中《C++程序设计语言》被认为是C++语言的”圣经”。Stroustrup以语言之父的口吻在书中详细介绍了C++的语言特性、抽象机制、标准库与设计理念。它不仅是一本语言参考,更是理解C++哲学的重要资源。

我是从高教影印版的《The C++ Programming Language (Special Edition)》开始看这本书的,与当时手里的钱能老师所著的《C++程序设计教程》相比,我感觉Stroustrup的The C++ Programming Language简直是在讲述一门新语言。

Stroustrup的另外一本书《The Design and Evolution of C++》是C++进阶的必读之作,国内版译为《C++语言的设计与演化》,这本书可以理解为Stroustrup设计C++背后的心路历程以及设计决策与语言机制:

Stroustrup的书虽好,但读起来有些难度,对初学者可能不那么友好,尤其是The C++ Programming Language,更像是一本C++语言的spec,缺少了像Kernighan那种春风化雨的阅读体验,所以我个人更喜欢下面这位C++大佬的作品。

注:Stroustrup这些年持续更新其作品,甚至还推出了《A Tour of C++》这样的更易读的小册子。

2.2.2 Stanley B. Lippman

Stanley B. Lippman是Stroustrup的同事,早年和Stroustrup一起在Bell实验室开发C++编译器,2001年,Lippman加入微软,成为Visual C++的架构师。他最为人所称道的是他的“一厚一薄”两本C++经典著作。

我们先说这本厚的,它就是C++大部头:《C++ Primer》,这本书分为C++基础、C++标准库、类设计者的工具和高级主题四个部分,非常适合C++初学者,同样其高级主题对于有经验的C++熟手也有很高的价值。

Lippman的另外一本薄书名为《Inside the C++ Object Model》,最初国内中译版《深度探索C++对象模型》由宝岛知名技术作家侯捷翻译,如今的很多新一代程序员可能已经不知道侯捷老师了,他如今依然活跃在C++高级培训的舞台上。

这本书属于C++进阶书籍,Lippman从C++编译器实现者的角度对C++的对象模型、继承和多态的实现机制(比如虚函数表、动态绑定等)等做了深入浅出的讲解,是C++走向高级阶段的必读之作。

不幸的是,Lippman已于2022年仙逝,我们再也看不到他亲自更新C++ Primer了。

2.2.3 Scott Meyers

如果你学过C++,但没有看过Effective C++系列,那我可以肯定你不是C++高手,Scott Meyers的《Effective C++》系列书籍是C++程序员通往高手境界的必读书籍:

这套C++丛书的特色就是以一条条C++准则为单元,每一条都扼要说明了一个可让你写出更好的C++程序代码的方法,并以特别设计过的例子详加讨论,这非常适合程序员的胃口。

2.3 Java

我在工作初期曾经系统学过Java,那时Java刚刚发布5.0,Spring也是方兴未艾。现在看来,没有Spring的Java是那么的纯粹!

学习纯Java,两本书足矣!下面我们就分别来看看这两本书和他们的作者。

2.3.1 Bruce Eckel

Bruce Eckel是著名的C++和Java作家,以其深入浅出的写作风格闻名。我没有将Eckel列到C++范畴,一是因为C++大神太多,二则是因为他的Thinking in Java似乎比他的Thinking in C++影响力更大。

这本书《Java编程思想》被誉为学习Java最全面的资源之一。Eckel以其特有的方式,深入浅出地解释了Java的核心概念和高级特性。书中的例子丰富而实用,帮助读者真正理解和掌握Java编程,并这本书只讲纯Java语法,并不涉及任何框架。读过的朋友,还记得书中那句“Everything is an object”吗!

2.3.2 Joshua Bloch

和Bruce Eckel是一个作家和培训师不同,Joshua Bloch领导了许多Java平台功能的设计和实现,包括Java Collections Framework、java.math包和断言机制等,对Java语言和库的发展做出了重要贡献。他曾在Sun Microsystems担任杰出工程师。2004年他离开Sun,成为Google首席Java架构师。

和Bloch为Java实现做出的贡献相比,他的书籍在Java界更是“家喻户晓”,他曾自己或与其他人合著过多本Java书籍,包括Java Puzzlers、Java Concurrency In Practice以及Effective Java。而最后的《Effective Java》更是成为了Java程序员几乎人手一本的神作:

这本书提供了编写高质量Java代码的最佳实践。Bloch基于自己丰富的经验,提出了许多实用的建议,涵盖了从基本的编程习惯到高级主题如并发和序列化,其中每条建议都值得大家细致琢磨品味。这本书帮助无数Java程序员提升了代码质量和效率。

3. 算法与数据结构

程序员,永远绕不开算法与数据结构。在算法与数据结构领域,Donald E. Knuth无疑是祖师爷级别的,他写的多卷本大部头的“计算机程序设计艺术”被多少人买回后顶礼膜拜,却不曾拆封拜读:)。

更多人和我一样,喜欢更为实用的,能看懂的书籍资料。

3.1 Robert Sedgewick

首先我们来看Sedgewick和Wayne合著的作品:《算法(第4版)》。

Robert Sedgewick是Donald E. Knuth的学生,名门之后,从1985年开始一直担任普林斯顿大学计算机科学系教授,曾任该系主任。很多耳熟能详的数据结构和算法都是Sedgewick发明的,比如红黑树、三元搜索树等。他基于课程讲义编写的这本“算法”,以清晰的讲解和丰富的Java实现而闻名。该书不仅介绍了经典数据结构和算法,还着重讨论了算法在实际问题中的应用。书中包含了大量的图示和代码,使得复杂的算法概念变得易于理解。这本书适合从入门到进阶的各个阶段的读者,是算法学习的必备参考。不过你不要想一下吃透这本书,很多算法非常深奥,可以将其作为案头的参考书,常看常新。

Sedgewick曾出版过多本算法书籍,有C实现的,有C++实现的,大家可以根据自己需要选择不同的实现版本。

3.2 Thomas H. CormenCharles E. Leiserson

提到算法,就不能不提到另外一部大部头的经典著作《算法导论

这部作品的英文版有上千页,可谓是算法领域的“百科全书”,这本书由 达特茅斯学院计算机科学系教授Thomas H. Cormen、麻省理工学院计算机科学与电气工程系教授Charles E. Leiserson等四人共同完成。这本书既全面又严谨,因此啃起来非常有难度,我在大学时期就买了该书的高教出版社的影印版,至今过去了十余年,我也没有完成全书的阅读:(。

在国内数据结构领域不得不说的另外一本教材是清华大学出版社出版的、由严蔚敏和吴伟民两位老师合著的《数据结构(C语言版)》,因很多高效将其作为考研指定教材,因此这本书的市占率很高,大家可以结合前面两个外版教材一起学习,效果可能更佳。下图是当年我购买时的版本样式:

4. 软件工程与编程思想

从大学毕业,入职工作后,软件工程知识必不可少,下面这些经典著作可以帮助大家快速融入工程领域。

4.1 Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides

这四位博士都是国际公认的面向对象软件领域的专家。他们在1994年合著的开创性的书籍《设计模式:可复用面向对象软件的基础》成为了开发人员在工程领域的必读之作,其影响力之广泛在整个IT领域都能排在TOP10。

这本书定义并系统化了软件设计中的常见模式,为面向对象设计提供了一套通用词汇和最佳实践。书中详细描述了23种设计模式,并通过实例说明了它们的应用场景。这本书不仅影响了无数程序员的设计思想,也为软件工程领域提供了宝贵的参考。这四位博士的工作对软件设计模式的研究和应用产生了深远的影响。

4.2 Steve McConnell

Steve McConnell是软件工程实践领域的权威专家,他的著作有不少,包括《Code Complete》、《Rapid Development 》和《Software Estimation》等,都对提高代码质量和开发效率有着重要影响。而其中影响力最大的莫过于《代码大全(第2版)》:

这是一本软件构建实践的百科全书,它涵盖了从变量命名到软件架构的各个方面。McConnell以丰富的经验和洞察力,提供了大量实用的编程技巧和最佳实践。这本书不仅适合新手学习,也是有经验的程序员提升技能的重要资源。并且,书中所讲的各种技巧和实践几乎与编程语言无关,无论你擅长哪种语言,都能从中获益!

4.3 Robert C. Martin(Uncle Bob)

Robert C. Martin,昵称”Uncle Bob”,是敏捷开发运动的重要推动者,也是软件工艺的倡导者。他的著作颇多,包括敏捷软件开发、敏捷整洁之道、代码整洁之道、匠艺整洁之道等。最近刚刚上市的《函数式设计》也出自Bob大叔之手。

在他的诸多作品中,《敏捷软件开发:原则、模式与实践》对我的影响更为深刻。

在这本书中,Martin详细阐述了敏捷开发的核心原则(SOLID原则),并通过大量的案例研究和设计模式,展示了如何在实践中应用这些原则。这本书不仅介绍了技术层面的最佳实践,还深入探讨了敏捷开发对团队协作和项目管理的影响。

4.4 Andrew HuntDavid Thomas

Hunt和Thomas是两位经验丰富的软件开发者,他们的著作强调了持续学习和改进在程序员职业生涯中的重要性。他们共同开创了Pragmatic Programmer的概念,并通过其大作:《程序员修炼之道:从小工到专家》为开发人员讲述具体实践的方法:

这本书强调了在软件开发中保持务实态度的重要性。作者们通过一系列小贴士和练习,涵盖了从个人责任到知识投资等多个方面,帮助程序员不断提升自己的技能和职业素养。

4.5 Frederick P. Brooks Jr.

谈到软件工程,我们不能忘记一个人,他就是Frederick P. Brooks Jr.。Brooks是一位美国计算机架构师、软件工程师和计算机科学家,以管理IBMSystem/360系列大型机和OS/360的开发而闻名。他在其开创性著作《人月神话》中坦率地写下了这些开发和项目管理经历,对后续的软件工程领域产生了深远的影响:

这本软件工程的经典之作挑战了许多关于软件开发的传统观念。Brooks通过自己在IBM的经历,深入探讨了大型软件项目管理中的各种问题。尽管首次出版已经过去多年,但书中关于团队沟通、项目规划和概念完整性等方面的见解至今仍然适用,是每个软件项目管理者入门必读的著作。

5. 计算机系统

最后,我们看一下计算机系统领域,我将系统编程、网络编程、编译器、数据库、操作系统统统放到这个领域一起说明了,排名不分先后:)。

5.1 Randal E.Bryant

Randal Bryant是一位美国计算机科学家和学者,因其在形式验证数字硬件和软件方面的研究而闻名。Bryant自1984年以来一直在卡内基梅隆大学任教。2004年至2014年,他担任卡内基梅隆大学计算机科学学院(SCS)院长。他长期从事本科生和研究生计算机系统方面课程教学近40年。他和David O’Hallaron教授一起在卡内基梅隆大学开设了15-213课程“计算机系统导论”,其《深入理解计算机系统》便是以这门课的讲义为基础撰写而成的:

这本书涵盖了计算机系统的多个层面,包括硬件、操作系统、编程语言和网络等,使读者对计算机的整体架构有深入的理解。对于计算机专业入门的学生而言,这本书是必读的教材,国内尚没有类似的教材能望其项背!当年如果早早能看到这本教材该多好啊!

5.2 W. Richard Stevens

Richard Stevens是UNIX和网络编程领域的权威专家,也是我顶礼膜拜的大神,他的著作对系统级编程产生了深远的影响。在我工作后的若干年内,Stevens的作品是我理解Unix/Linux系统编程的必备参考,并全部购买收藏,随时翻阅。更神奇的是,他的每一部作品都是上乘之作,看下面的豆瓣评分:

-《UNIX环境高级编程》

这本书被誉为UNIX编程的”圣经”。Stevens深入浅出地解释了UNIX系统调用和库函数的使用,涵盖了文件I/O、进程控制、信号处理、线程等关键主题。这本书不仅是学习UNIX/Linux系统编程的必备参考,也为理解操作系统内部工作原理提供了宝贵的见解。

-《UNIX网络编程》(卷1:套接字联网API,卷2:进程间通信)

相对于Unix环境高级编程的全面和总括,这套书深入具体领域,重点覆盖了UNIX环境下的网络编程和进程间通信技术。第一卷重点讲解了TCP/IP协议族和套接字编程,第二卷则专注于UNIX系统上的各种IPC(进程间通信)机制。这套书不仅提供了详细的技术讲解,还包含了大量的实例代码,是网络编程学习和实践的必备参考。

-《TCP/IP详解》系列

这套书深入浅出地解释了TCP/IP协议族的工作原理,从协议的基本概念到复杂的实现细节,为读者呈现了一幅完整的TCP/IP知识图谱。这套书不仅适合网络程序员阅读,也是理解现代互联网技术基础的重要资源。

对于Stevens的这些书,虽然年代已久,但对如今的后端/系统程序员依然有极大的参考价值,建议大家必读。

5.3 Alfred V. Aho, Monica S. Lam, Ravi Sethi和Jeffrey D. Ullman

以Alfred V. Aho为代表的这几位作者都是编译器理论和实现的权威专家,他们的著作被誉为编译原理领域的”圣经”。Alfred V. Aho同时也是AWK语言中的那个”A”,他还著有《计算机算法的设计与分析》。当然“龙书”是其在学术领域著作的最卓越代表,学编译原理的同学建议人手一本。

这本书以其全面性和深度在编译器领域独树一帜。从词法分析、语法分析到代码优化,书中详细讲解了编译器设计的各个环节。虽然以理论为主,但书中也包含了大量的实例和练习,帮助读者将理论付诸实践。这本书不仅是编译器开发者的必读之作,对理解程序语言的设计和实现也有重要帮助。国内各大开设编译原理课程的重点高校也都将其作为第一教材。国内一些高校也编写了一些自己的教材,但与这本“龙书”相比,level还是差距很大。

5.4 Abraham Silberschatz

Avi Silberschatz是一位以色列计算机科学家和研究员,曾在bell实验室工作过,他因在计算机科学领域撰写了许多有影响力的著作而闻名,尤其是操作系统和数据库系统方面。其作品《数据库系统概念》《操作系统概念》被全世界的高校计算机专业所采用。

-《数据库系统概念》

本书由Abraham Silberschatz、 Henry F. Korth和S. Sudarshan合著,这三位作者都是数据库系统领域的专家,他们的著作被广泛用作大学教材和专业参考。这本书全面介绍了数据库系统的基本概念、设计原理和实现技术。从关系代数到事务处理,从查询优化到分布式数据库,书中涵盖了传统和现代数据库技术的各个方面。无论你是在校数据库专业的学生,还是从事数据库核心系统开发的工程师,亦或是数据库应用开发的程序员,本书都极具参考价值,可放置在案头随时查看。

-《操作系统概念》

本书由Abraham Silberschatz, Peter B. Galvin 和Greg Gagne合著,这几位作者都是操作系统理论和实践的专家,他们的著作在学术界和工业界都有广泛影响。

这本书以其全面性和深度成为了操作系统学习的重要参考。从进程管理到分布式系统,从内存管理到安全性,书中详细讨论了操作系统的各个方面。作者们不仅介绍了理论知识,还通过案例研究展示了这些概念在实际系统中的应用。这本书适合从入门到进阶的各个阶段的读者,是理解现代计算机操作系统工作原理的关键参考材料。

6. 小结

在教师节这个神圣的日子中,我们回顾了这些在计算机科学和编程领域做出杰出贡献的”老师们”。他们的智慧和洞见,通过这些经典著作,影响了几代程序员的成长,更是对我的程序员生涯提供了莫大的帮助。

这些大师们不仅仅传授了技术知识,更重要的是,他们塑造了我们思考问题和解决问题的方式。从C语言到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