1. 使能cgo的同时也生成static build

LDFLAGS += -linkmode=external -extldflags=-static
CGO_ENABLED=1 go build -ldflags="$(LDFLAGS)"

要点:

  • gcc要是musl based, 比如
    • 直接使用alpine linux
    • 在ubuntu上安装musl:
      sudo apt-get install -y --no-install-recommends musl-dev musl-tools
      LDFLAGS += -linkmode=external -extldflags=-static
      CC=musl-gcc CGO_ENABLED=1 go build -ldflags="$(LDFLAGS)"
      

2. setns系统调用和runtime.LockOSThread()

2.1. setns

2.1.1. 函数原型

#define _GNU_SOURCE             /* See feature_test_macros(7) */
#include <sched.h>

int setns(int fd, int nstype);

2.1.2. 七种名字空间类型

setns系统调用重新绑定caller线程到目标名字空间. 目标名字空间由fd指定, 这个fd是目标pid下面的/proc/[pid]/ns/的7种名字空间之一.

Namespace   Constant          Isolates
Cgroup      CLONE_NEWCGROUP   Cgroup root directory
IPC         CLONE_NEWIPC      System V IPC, POSIX message queues
Network     CLONE_NEWNET      Network devices, stacks, ports, etc.
Mount       CLONE_NEWNS       Mount points
PID         CLONE_NEWPID      Process IDs
User        CLONE_NEWUSER     User and group IDs
UTS         CLONE_NEWUTS      Hostname and NIS domain name

clone, setns, unshare系统调用能处理CLONE_NEW* flag.

$ ls /proc/1770/ns -lh
total 0
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 net -> 'net:[4026531993]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 pid -> 'pid:[4026531836]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 user -> 'user:[4026531837]'
lrwxrwxrwx 1 yingjieb platform 0 Apr  2 01:02 uts -> 'uts:[4026531838]'

可以看到, linux的名字空间都是和pid绑定的, 但7个名字空间是独立的. 但是, 即使该名字空间里面的所有进程都退出了, 这个名字空间还可能继续存在:

  • 如果/proc/pid/ns下面的文件被bind mount
  • 如果/proc/pid/ns下面的文件还在被其他进程open

名字空间的个数有限制, 比如

$ ls /proc/sys/user
max_cgroup_namespaces  max_ipc_namespaces  max_pid_namespaces
max_inotify_instances  max_mnt_namespaces  max_user_namespaces
max_inotify_watches    max_net_namespaces  max_uts_namespaces

$ cat /proc/sys/user/max_mnt_namespaces
289679

2.1.3. PID 名字空间

PID名字空间有点特殊, setns不会把calling线程put到目标PID空间, 而是把calling线程的接下来的子进程放到目标PID空间.

2.1.4. 权限

调用线程需要有CAP_SYS_ADMIN权限

2.2. 其他

查看所有名字空间

lsns

2.3. mount名字空间的问题以及golang的解决方案

多线程的程序可能不能setns到mount名字空间. 另外, 除了CAP_SYS_ADMIN, mount名字空间还需要CAP_SYS_CHROOT权限.

golang里面, 因为goroutine可以在不同线程间被调度, 而setns需要以线程为基础, 所以setns到mount名字空间会返回invalid argument

#include <errno.h>
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>

main(int argc, char* argv[]) {
    int i;
    char nspath[1024];
    char *namespaces[] = { "ipc", "uts", "net", "pid", "mnt" };

    if (geteuid()) { fprintf(stderr, "%s\n", "abort: you want to run this as root"); exit(1); }

    if (argc != 2) { fprintf(stderr, "%s\n", "abort: you must provide a PID as the sole argument"); exit(2); }

    for (i=0; i<5; i++) {
        sprintf(nspath, "/proc/%s/ns/%s", argv[1], namespaces[i]);
        int fd = open(nspath, O_RDONLY);

        if (setns(fd, 0) == -1) { 
            fprintf(stderr, "setns on %s namespace failed: %s\n", namespaces[i], strerror(errno));
        } else {
            fprintf(stdout, "setns on %s namespace succeeded\n", namespaces[i]);
        }

        close(fd);
    }
}

//sudo ./checkns <PID>
//output:
setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace succeeded

对应的go代码会出现错误

package main

import (
    "fmt"
    "os"
    "path/filepath"
    "syscall"
)

func main() {
    if syscall.Geteuid() != 0 {
        fmt.Println("abort: you want to run this as root")
        os.Exit(1)
    }

    if len(os.Args) != 2 {
        fmt.Println("abort: you must provide a PID as the sole argument")
        os.Exit(2)
    }

    namespaces := []string{"ipc", "uts", "net", "pid", "mnt"}

    for i := range namespaces {
        fd, _ := syscall.Open(filepath.Join("/proc", os.Args[1], "ns", namespaces[i]), syscall.O_RDONLY, 0644)
        err, _, msg := syscall.RawSyscall(308, uintptr(fd), 0, 0) // 308 == setns

        if err != 0 {
            fmt.Println("setns on", namespaces[i], "namespace failed:", msg)
        } else {
            fmt.Println("setns on", namespaces[i], "namespace succeeded")
        }

    }
}

//sudo go run main.go <PID>
//output:
setns on ipc namespace succeeded
setns on uts namespace succeeded
setns on net namespace succeeded
setns on pid namespace succeeded
setns on mnt namespace failed: invalid argument

stackoverflow的讨论

runc的源代码中, https://github.com/opencontainers/runc/blob/main/libcontainer/process_linux.go
函数func (p *initProcess) goCreateMountSources(ctx context.Context) (mountSourceRequestFn, context.CancelFunc, error)中似乎解决了这个问题:

// goCreateMountSources spawns a goroutine which creates open_tree(2)-style
// mountfds based on the requested configs.Mount configuration. The returned
// requestFn and cancelFn are used to interact with the goroutine.
//
// The caller of the returned mountSourceRequestFn is responsible for closing
// the returned file.
func (p *initProcess) goCreateMountSources(ctx context.Context) (mountSourceRequestFn, context.CancelFunc, error) {
    type response struct {
        src *mountSource
        err error
    }

    errCh := make(chan error, 1)
    requestCh := make(chan *configs.Mount)
    responseCh := make(chan response)

    ctx, cancelFn := context.WithTimeout(ctx, 1*time.Minute)
    go func() {
        // We lock this thread because we need to setns(2) here. There is no
        // UnlockOSThread() here, to ensure that the Go runtime will kill this
        // thread once this goroutine returns (ensuring no other goroutines run
        // in this context).
        runtime.LockOSThread()

        // Detach from the shared fs of the rest of the Go process in order to
        // be able to CLONE_NEWNS.
        if err := unix.Unshare(unix.CLONE_FS); err != nil {
            err = os.NewSyscallError("unshare(CLONE_FS)", err)
            errCh <- fmt.Errorf("mount source thread: %w", err)
            return
        }

        // Attach to the container's mount namespace.
        nsFd, err := os.Open(fmt.Sprintf("/proc/%d/ns/mnt", p.pid()))
        if err != nil {
            errCh <- fmt.Errorf("mount source thread: open container mntns: %w", err)
            return
        }
        defer nsFd.Close()
        if err := unix.Setns(int(nsFd.Fd()), unix.CLONE_NEWNS); err != nil {
            err = os.NewSyscallError("setns", err)
            errCh <- fmt.Errorf("mount source thread: join container mntns: %w", err)
            return
        }

        // No errors during setup!
        close(errCh)
        logrus.Debugf("mount source thread: successfully running in container mntns")

        nsHandles := new(userns.Handles)
        defer nsHandles.Release()
    loop:
        for {
            select {
            case m, ok := <-requestCh:
                if !ok {
                    break loop
                }
                src, err := mountFd(nsHandles, m)
                logrus.Debugf("mount source thread: handling request for %q: %v %v", m.Source, src, err)
                responseCh <- response{
                    src: src,
                    err: err,
                }
            case <-ctx.Done():
                break loop
            }
        }
        logrus.Debugf("mount source thread: closing thread: %v", ctx.Err())
        close(responseCh)
    }()

    // Check for setup errors.
    err := <-errCh
    if err != nil {
        cancelFn()
        return nil, nil, err
    }

    // TODO: Switch to context.AfterFunc when we switch to Go 1.21.
    var requestChCloseOnce sync.Once
    requestFn := func(m *configs.Mount) (*mountSource, error) {
        var err error
        select {
        case requestCh <- m:
            select {
            case resp, ok := <-responseCh:
                if ok {
                    return resp.src, resp.err
                }
            case <-ctx.Done():
                err = fmt.Errorf("receive mount source context cancelled: %w", ctx.Err())
            }
        case <-ctx.Done():
            err = fmt.Errorf("send mount request cancelled: %w", ctx.Err())
        }
        requestChCloseOnce.Do(func() { close(requestCh) })
        return nil, err
    }
    return requestFn, cancelFn, nil
}

关键在go出去的routine里, 首先调用runtime.LockOSThread() 这里的注释

// We lock this thread because we need to setns(2) here. There is no
// UnlockOSThread() here, to ensure that the Go runtime will kill this
// thread once this goroutine returns (ensuring no other goroutines run
// in this context).

以及https://github.com/opencontainers/runc/tree/main/libcontainer/nsenter的readme:

The nsenter package registers a special init constructor that is called before the Go runtime has a chance to boot. This provides us the ability to setns on existing namespaces and avoid the issues that the Go runtime has with multiple threads. This constructor will be called if this package is registered, imported, in your go application.

The nsenter package will import "C" and it uses cgo package. In cgo, if the import of "C" is immediately preceded by a comment, that comment, called the preamble, is used as a header when compiling the C parts of the package. So every time we import package nsenter, the C code function nsexec() would be called. And package nsenter is only imported in init.go, so every time the runc init command is invoked, that C code is run.

Because nsexec() must be run before the Go runtime in order to use the Linux kernel namespace, you must import this library into a package if you plan to use libcontainer directly. Otherwise Go will not execute the nsexec() constructor, which means that the re-exec will not cause the namespaces to be joined. You can import it like this:

import _ "github.com/opencontainers/runc/libcontainer/nsenter"

3. select随机选择ready的case

adaptiveservice的chan transport实现里, 出现来的一个bug. 现象是丢消息. 最后调查下来和select的随机选择case有关.

3.1. 问题背景

client和server通过adaptiveservice的channel transport通信. client主动发起建立连接, 并在结束后关闭连接.

client代码逻辑简化如下:

//发现并连接server
c := NewClient().SetDiscoverTimeout(0)
conn := <-c.Discover(BuiltinPublisher, SrvMessageTracing)

//发送
tracedMsg := tracedMessageRecord{...}
conn.Send(&tracedMsg)
conn.Close()

bug现象是client发送的tracedMsg并没有被server处理.

3.2. server端的处理逻辑

server端遇到client发过来的握手请求, 会新建goroutine处理:

go func() {
    for {
        select {
            //监测是否client主动关闭了通道.
            //connClose由client持有
            case <-connClose:
                //关闭连接
                return
            //recvChan是server端给这个client新建的接收队列
            case tm := <-recvChan:
                //处理消息
        }
    }
}()

问题出在case <-connClose后的那个return上.
client几乎是发了消息马上关闭连接, 那么server端的select背后的epoll很可能会看到两个case几乎同时ready. 根据go的spec

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen. If there is no default case, the "select" statement blocks until at least one of the communications can proceed.

case的选择是随机的. 那么如果选择了case <-connClose, 走return流程, 那么recvChan里面还没来得及处理的消息都会丢失.

3.3. 如何修改

Alt text

在这个修改中, case <-connClose只是关闭recvChan; 而recvChan里面读空才return.

4. 交叉编译指定cc

CGO_ENABLED=1 GOARCH=arm64 CC=aarch64-alpine-linux-musl-gcc go build -buildmode=plugin -ldflags="-extld=aarch64-alpine-linux-musl-ld"

5. go mod tidy总是pull老版本

原因是go工具链使用了proxy, 有cache.
GOPRIVATE指定github不走proxy可以解决:

GOPRIVATE=github.com go get github.com/godevsig/grepo@master

6. update所有依赖的版本

GOPRIVATE=github.com go get -u ./...
GOPRIVATE=github.com go mod tidy

6.1. 手动更新指定package的非tag版本

go get -u只能更新package到tag版本, 如果需要更新的版本, 需要手动修改go.mod文件, 比如修改指定package为master分支:

github.com/godevsig/glib master

再用go mod tidy就可以更新到最新版本. 比如这里运行后, go.mod被自动修改为:

github.com/godevsig/glib v0.1.2-0.20230826061212-649e5e40e117

6.2. go mod的版本选择算法 Minimal version selection (MVS)

基本假设就是package满足向前兼容性原则, 新版本总是比老版本好.

所以MVS虽然名字里有minimal字样, 但实际上它倾向于选择新版本, 比如下图:

MVS produces the build list as output, the list of module versions used for a build.

MVS starts at the main modules (special vertices in the graph that have no version) and traverses the graph, tracking the highest required version of each module. At the end of the traversal, the highest required versions comprise the build list: they are the minimum versions that satisfy all requirements.

MVS visits and loads the go.mod file for each of the module versions highlighted in blue. At the end of the graph traversal, MVS returns a build list containing the bolded versions: A 1.2, B 1.2, C 1.4, and D 1.2. Note that higher versions of B and D are available but MVS does not select them, since nothing requires them.

go mod 选择算法

go list -m all可以查看这个build list.

The build list may be inspected with the command go list -m all. Unlike other dependency management systems, the build list is not saved in a “lock” file. MVS is deterministic, and the build list doesn’t change when new versions of dependencies are released, so MVS is used to compute it at the beginning of every module-aware command.

详见:

7. fmt打印颜色

func main() {
    // Print colored text
    fmt.Println("\x1b[31mRed Text\x1b[0m")
    fmt.Println("\x1b[32mGreen Text\x1b[0m")
    fmt.Println("\x1b[33mYellow Text\x1b[0m")
    fmt.Println("\x1b[34mBlue Text\x1b[0m")
    fmt.Println("\x1b[35mMagenta Text\x1b[0m")
    fmt.Println("\x1b[36mCyan Text\x1b[0m")
}

8. 升级go1.18遇到的问题

8.1. mips64运行时崩溃

运行环境: octeon3 CPU(mips64)
我把gshell的toolchain从1.16升级到了1.18, 小的程序运行没问题.
但gshell daemon运行出错:

~/gshell # bin_latest/gshell -loglevel info daemon -registry 10.182.105.179:11985 -bcast 9923
trap:5111, a123456=[0,0,0,0,0,0]
results: got {r1=0,r2=5111,errno=0}, want {r1=0,r2=0,errno=0
fatal error: AllThreadsSyscall6 results differ between threads; runtime corrupted
trap:5111, a123456=[0,0,0,0,0,0]
results: got {r1=0,r2=5111,errno=0}, want {r1=0,r2=0,errno=0
fatal error: AllThreadsSyscall6 results differ between threads; runtime corrupted


goroutine 0 [idle]:
runtime: unexpected return pc for runtime.sigtramp called from 0xffeccc3580
stack: frame={sp:0xc00004fc88, fp:0xc00004fcd0} stack=[0xc000048000,0xc000050000)
//省略栈打印
runtime.throw({0x649b1f, 0x44})
        /usr/local/go/src/runtime/panic.go:992 +0x6c
runtime.runPerThreadSyscall()
        /usr/local/go/src/runtime/os_linux.go:874 +0x410
runtime.sighandler(0x21, 0xc00004fce8, 0xc00004fd68, 0xc0000029c0)
        /usr/local/go/src/runtime/signal_unix.go:631 +0x770
runtime.sigtrampgo(0x21, 0xc00004fce8, 0xc00004fd68)
        /usr/local/go/src/runtime/signal_unix.go:477 +0x228
runtime: unexpected return pc for runtime.sigtramp called from 0xffeccc3580
stack: frame={sp:0xc00004fc88, fp:0xc00004fcd0} stack=[0xc000048000,0xc000050000)
//省略栈打印
runtime.sigtramp()
        /usr/local/go/src/runtime/sys_linux_mips64x.s:435 +0x54

goroutine 1 [running]:
        goroutine running on other thread; stack unavailable
trap:5111, a123456=[0,0,0,0,0,0]

goroutine 0 [idle]:
runtime: unexpected return pc for runtime.sigtramp called from 0xffeccc3580
stack: frame={sp:0xc00003fc88, fp:0xc00003fcd0} stack=[0xc000038000,0xc000040000)

runtime.throw({0x649b1f, 0x44})
        /usr/local/go/src/runtime/panic.go:992 +0x6c
runtime.runPerThreadSyscall()
        /usr/local/go/src/runtime/os_linux.go:874 +0x410
runtime.sighandler(0x21, 0xc00003fce8, 0xc00003fd68, 0xc0000024e0)
        /usr/local/go/src/runtime/signal_unix.go:631 +0x770
runtime.sigtrampgo(0x21, 0xc00003fce8, 0xc00003fd68)
        /usr/local/go/src/runtime/signal_unix.go:477 +0x228
runtime: unexpected return pc for runtime.sigtramp called from 0xffeccc3580
stack: frame={sp:0xc00003fc88, fp:0xc00003fcd0} stack=[0xc000038000,0xc000040000)
runtime.sigtramp()
        /usr/local/go/src/runtime/sys_linux_mips64x.s:435 +0x54

这个error message意思是说r2寄存器应该是0, 但实际却是5111

8.1.1. 解决

根据https://github.com/golang/go/issues/56426, 这是go1.18引入的bug, 1.20+才fix. 看来要backporting 这个patch了.

All mips variant perform syscalls similarly. R2 (v0) holds r1 and R3 (v1) holds r2 of a syscall. The latter is only used by 2-ret syscalls. A 1-ret syscall would not touch R3 but keeps it as is, making r2 be a random value. Always reset it to 0 before SYSCALL to fix the issue.

9. 如何正确静态链接

参考:

9.1. 结论

Dynamically-linked binary Statically-linked binary
Libc functions go build go build -ldflags "-linkmode 'external' -extldflags '-static'"
Golang functions n/a CGO_ENABLED=0 go build

I.e.

  • To get a static binary, under most circumstances: CGO_ENABLED=0 go build
  • To use the libc functions for net and os/user, and still get a static binary (for containers): go build -ldflags "-linkmode 'external' -extldflags '-static'"

10. 不能在for range里删除slice元素

我问chatgpt如何在for range里删除slice元素, chatgpt给出的代码:

package main

import "fmt"

func main() {
    // Create a slice of integers
    nums := []int{1, 2, 3, 4, 5}

    // Iterate over the slice using range
    for i, num := range nums { // for range在一开始就定好了要interate多少次
        fmt.Println(i, num, len(nums)) // 而且不受index越界限制
        //fmt.Println(i, num, nums[i], len(nums)) // 我加的这行会panic
        // Check if the element should be deleted
        if num == 3 || num == 4 {
            // Delete the element from the slice using append
            nums = append(nums[:i], nums[i+1:]...)
        }
    }

    // Print the updated slice
    fmt.Println(nums)
}

//结果
0 1 5
1 2 5
2 3 5
3 5 4
4 5 4 //注意看这行, 实际上新的nums已经没有index 4了, for range里面的i和num还是能在这一轮取到值
[1 2 4 5]

如果告诉chatgpt答案是错的, 它会修正代码:

ME: the output is [1 2 4 5], which is not correct

CHATGPT: You are correct, I apologize for the mistake in my previous answer. When you delete elements from a slice while iterating over it with range, you need to adjust the index variable to account for the removed elements.

再给出的答案是对的:

package main

import "fmt"

func main() {
    // Create a slice of integers
    nums := []int{1, 2, 3, 4, 5}

    // Iterate over the slice using range
    for i := 0; i < len(nums); i++ {
        num := nums[i]
        // Check if the element should be deleted
        if num == 3 || num == 4 {
            // Delete the element from the slice
            nums = append(nums[:i], nums[i+1:]...)
            // Adjust the index variable to account for the removed element
            i--
        }
    }

    // Print the updated slice
    fmt.Println(nums)
}

11. 什么时候用buffer为1的channel

比如下面的代码:

package main

import (
    "errors"
    "fmt"
    "time"
)

func main() {
    errorChan := make(chan error, 1)
    fmt.Println("start")
    time.AfterFunc(time.Second*5, func() { errorChan <- nil; fmt.Println("time out") })

    go func() {
        time.Sleep(time.Second * 3) // 实际场景可以是cmd.Wait()
        errorChan <- errors.New("new error")
        fmt.Println("error happened")
    }()

    fmt.Println(<-errorChan)

    time.Sleep(time.Second * 10)
    fmt.Println("done")
}

12. go按位取反(bitwise not)

go没有专用的取反操作符, 但用异或可以取反:

func main() {
    var bitwisenot byte = 0x0F

    // printing the number in 8-Bit
    fmt.Printf("%08b\n", bitwisenot) // 00001111

    fmt.Printf("%08b\n", ^bitwisenot) // 11110000
    fmt.Printf("%08b\n", 1^bitwisenot) // 00001110 和上面结果不一样

    fmt.Printf("%08b\n", ^0x0F) // -0010000 默认数字都是int
    fmt.Printf("%08b\n", ^(int)(0x0F)) // -0010000
    fmt.Printf("%08b\n", ^(uint)(0x0F)) // 1111111111111111111111111111111111111111111111111111111111110000 不带符号位
}

结果:

00001111
11110000
00001110
-0010000
-0010000
1111111111111111111111111111111111111111111111111111111111110000

13. go的相等性(==)

首先, map和slice不能用==比较 function也不能比较.

f := func(int) int { return 1 }
g := func(int) int { return 2 }
f == g
//这样比较会编译错误

但function可以跟nil比较.

13.1. 普通类型的比较

boo, int, float, complex的比较就是普通比较. 但需要注意的是float的NaN不等于NaN

nan := math.NaN()
pos_inf := math.Inf(1)
neg_inf := math.Inf(-1)
fmt.Println(nan == nan)         // false
fmt.Println(pos_inf == pos_inf) // true
fmt.Println(neg_inf == neg_inf) // true
fmt.Println(pos_inf == neg_inf) // false

13.2. 指针的相等性

要么两个指针都是nil, 要么两个指针指向同样的地址:

var p1, p2 *string
name := "foo"
fmt.Println(p1 == p2) // true
p1 = &name
p2 = &name
fmt.Println(p1)       // 0x40c148
fmt.Println(p2)       // 0x40c148
fmt.Println(&p1)      // 0x40c138
fmt.Println(&p2)      // 0x40c140
fmt.Println(*p1)      // foo
fmt.Println(*p2)      // foo
fmt.Println(p1 == p2) // true

需要注意的是, 两个不同的empty struct(即空的struct实例)的地址可能相等

A struct or array type has size zero if it contains no fields (or elements, respectively) that have a size greater than zero. Two distinct zero-size variables may have the same address in memory.

type S struct{}
func main() {
    var p1, p2 *S
    s1 := S{}
    s2 := S{}
    p1 = &s1
    p2 = &s2
    fmt.Printf("%p\n", p1) // 0x1e52bc
    fmt.Printf("%p\n", p2) // 0x1e52bc
    fmt.Println(p1)        // &{}
    fmt.Println(p2)        // &{}
    fmt.Println(&p1)       // 0x40c138
    fmt.Println(&p2)       // 0x40c140
    fmt.Println(*p1)       // {}
    fmt.Println(*p2)       // {}
    fmt.Println(p1 == p2)  // true 本来s1和s2不是一个东西, 当都是空, 他们的地址相同, 所以相等.
}

如果结构体非空, S struct {f int}, p1p2就不相等了.

13.2.1. errors.New("EOF")不等于io.EOF

io.EOF在io包里定义为

var EOF = errors.New("EOF")

New出来的对象是个指针

package errors

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

根据指针的相等性, 其指向的地址相等才算相等.
下面的代码显示虽然myErr和io.EOF都是同样的内容, 但二者并不相等.

package main

import (
    "errors"
    "fmt"
    "io"
)

func main() {
    myErr := errors.New("EOF")
    fmt.Printf("%#v\n", myErr)
    fmt.Printf("%#v\n", io.EOF)
    if myErr == io.EOF {
        fmt.Println("equal")
    } else {
        fmt.Println("not equal")
    }
}

//输出
&errors.errorString{s:"EOF"}
&errors.errorString{s:"EOF"}
not equal

13.3. channel的相等性

满足下面条件之一

  • 两个chnnel都是nil
  • 两个都是从同一个make函数生成的
func f(ch1 chan int, ch2 *chan int) {
    fmt.Println(ch1 == *ch2) // true
}
func main() {
    var ch1, ch2 chan int
    fmt.Println(ch1 == ch2) // true
    ch1 = make(chan int)
    ch2 = make(chan int)
    fmt.Println(ch1 == ch2) // false
    ch2 = ch1
    fmt.Printf("%p\n", &ch1) // 0x40c138
    fmt.Printf("%p\n", &ch2) // 0x40c140
    fmt.Println(ch1 == ch2)  // true
    f(ch1, &ch1)
}

13.4. interface的相等性

  • 两个interface都是nil(注意动态类型也要是nil)
    type I interface{ m() }
    type T []byte
    func (t T) m() {}
    func main() {
      var t T
      fmt.Println(t == nil) // true
      var i I = t
      fmt.Println(i == nil)                   // false
      fmt.Println(reflect.TypeOf(i))          // main.T
      fmt.Println(reflect.ValueOf(i).IsNil()) // true
    }
    
  • 动态类型相同, 并且动态值相等
    type A int
    type B = A
    type C int
    type I interface{ m() }
    func (a A) m() {}
    func (c C) m() {}
    func main() {
      var a I = A(1)
      var b I = B(1)
      var c I = C(1)
      fmt.Println(a == b) // true 这里A和B是强别名(=号别名), 类型是一样的.
      fmt.Println(b == c) // false 类型不同不相等
      fmt.Println(a == c) // false 类型不同不相等
    }
    

类型I的interface变量i可以和普通类型X的实例x比较, 只要

  • 类型X实现了接口I
  • 类型X可以比较

所以i和x比较, 如果i的动态类型是X, i的动态值又等于x, 那么i和x相等

type I interface{ m() }
type X int
func (x X) m() {}
type Y int
func (y Y) m() {}
type Z int
func main() {
    var i I = X(1)
    fmt.Println(i == X(1)) // true
    fmt.Println(i == Y(1)) // false
    // fmt.Println(i == Z(1)) // mismatched types I and C
    // fmt.Println(i == 1) // mismatched types I and int
}

如果动态类型相等, 但这个类型不能比较, 则会产生panic:

type A []byte
func main() {
    var i interface{} = A{}
    var j interface{} = A{}
    fmt.Println(i == j)
}

panic: runtime error: comparing uncomparable type main.A

如果动态类型不一样, 那就直接不等:

type A []byte
type B []byte
func main() {
    // A{} == A{} // slice can only be compared to nil
    var i interface{} = A{}
    var j interface{} = B{}
    fmt.Println(i == j) // false
}

13.5. 结构体的相等性

首先, 结构体可以直接用==操作符比较. 如果里面的非_域都相等, 则两个结构体相等. 注意, 结构体里面的大写, 小写域都要相等.

type A struct {
    _ float64
    f1 int
    F2 string
}
type B struct {
    _ float64
    f1 int
    F2 string
}
func main() {
    fmt.Println(A{1.1, 2, "x"} == A{0.1, 2, "x"}) // true
    // fmt.Println(A{} == B{}) // mismatched types A and B 
}

当判断x==y时, 只有x可以赋值给y或者y可以赋值给x才能用==操做符. 所以下面的判断是不行的, 编译时就会报错.

A{} == B{}

13.6. Array的相等性

注意这里说的是Array, 不是slice. Array里面的每个元素都相等的话, 两个array相等.

type T struct {
    name string
    age int
    _ float64
}
func main() {
   x := [...]float64{1.1, 2, 3.14}
   fmt.Println(x == [...]float64{1.1, 2, 3.14}) // true
   y := [1]T{{"foo", 1, 0}}
   fmt.Println(y == [1]T{{"foo", 1, 1}}) // true
}

13.7. string

string的比较按照[]byte按字节比较.

fmt.Println(strings.ToUpper("ł") == "Ł")     // true
fmt.Println("foo" == "foo")                  // true
fmt.Println("foo" == "FOO")                  // false
fmt.Println("Michał" == "Michal")            // false
fmt.Println("żondło" == "żondło")            // true
fmt.Println("żondło" != "żondło")            // false
fmt.Println(strings.EqualFold("ąĆź", "ĄćŹ")) // true

13.8. []byte用bytes.Equal比较

切片不能直接比较. 但bytes.Equal可以比较两个[]byte

s1 := []byte{'f', 'o', 'o'}
s2 := []byte{'f', 'o', 'o'}
fmt.Println(bytes.Equal(s1, s2)) // true
s2 = []byte{'b', 'a', 'r'}
fmt.Println(bytes.Equal(s1, s2)) // false
s2 = []byte{'f', 'O', 'O'}
fmt.Println(bytes.EqualFold(s1, s2)) // true
s1 = []byte("źdźbło")
s2 = []byte("źdŹbŁO")
fmt.Println(bytes.EqualFold(s1, s2)) // true
s1 = []byte{}
s2 = nil
fmt.Println(bytes.Equal(s1, s2)) // true

13.9. reflect.DeepEqual万能比较

func DeepEqual(x, y interface{}) bool可以比较任意两个值. 比如map

m1 := map[string]int{"foo": 1, "bar": 2}
m2 := map[string]int{"foo": 1, "bar": 2}
// fmt.Println(m1 == m2) // map can only be compared to nil
fmt.Println(reflect.DeepEqual(m1, m2)) // true
m2 = map[string]int{"foo": 1, "bar": 3}
fmt.Println(reflect.DeepEqual(m1, m2)) // false
m3 := map[string]interface{}{"foo": [2]int{1,2}}
m4 := map[string]interface{}{"foo": [2]int{1,2}}
fmt.Println(reflect.DeepEqual(m3, m4)) // true
var m5 map[float64]string
fmt.Println(reflect.DeepEqual(m5, nil)) // false
fmt.Println(m5 == nil) // true

比如slice

s := []string{"foo"}
fmt.Println(reflect.DeepEqual(s, []string{"foo"})) // true
fmt.Println(reflect.DeepEqual(s, []string{"bar"})) // false
s = nil
fmt.Println(reflect.DeepEqual(s, []string{})) // false
s = []string{}
fmt.Println(reflect.DeepEqual(s, []string{})) // true

比如结构体

type T struct {
    name string
    Age  int
}
func main() {
    t := T{"foo", 10}
    fmt.Println(reflect.DeepEqual(t, T{"bar", 20})) // false
    fmt.Println(reflect.DeepEqual(t, T{"bar", 10})) // false
    fmt.Println(reflect.DeepEqual(t, T{"foo", 10})) // true
}

13.10. cmp包

google提供了cmp包, 可以打印两个值的差异

import (
    "fmt"
    "github.com/google/go-cmp/cmp"
)
type T struct {
    Name string
    Age  int
    City string
}
func main() {
    x := T{"Michał", 99, "London"}
    y := T{"Adam", 88, "London"}
    if diff := cmp.Diff(x, y); diff != "" {
        fmt.Println(diff)
    }
}

输出

  main.T{
-       Name: "Michał",
+       Name: "Adam",
-       Age:  99,
+       Age:  88,
        City: "London",
  }

14. 通过unix socket发送fd

gvisor的pkg/unet/unet.go里面提供了listen, accept等方法 unet是给server端用的. 比如read和write方法, 先尝试用阻塞式的unix.RawSyscall(unix.SYS_RECVMSG, ...), 不行再用对fd的pollunix.Syscall6(unix.SYS_PPOLL, ...)

还提供了通过unix socket发送/接收fd的方法:

// PackFDs packs the given list of FDs in the control message.
//
// This must be used prior to WriteVec.
func (c *ControlMessage) PackFDs(fds ...int) {
    *c = ControlMessage(unix.UnixRights(fds...))
}

// ExtractFDs returns the list of FDs in the control message.
//
// Either this or CloseFDs should be used after EnableFDs.
func (c *ControlMessage) ExtractFDs() ([]int, error) {
    msgs, err := unix.ParseSocketControlMessage(*c)
    if err != nil {
        return nil, err
    }
    var fds []int
    for _, msg := range msgs {
        thisFds, err := unix.ParseUnixRights(&msg)
        if err != nil {
            // Different control message.
            return nil, err
        }
        for _, fd := range thisFds {
            if fd >= 0 {
                fds = append(fds, fd)
            }
        }
    }
    return fds, nil
}

被extract出来的fd可以用比如下面的函数来生成一个File对象.

//比如调用
os.NewFile(uintptr(fd), "urpc")

// NewFile returns a new File with the given file descriptor and
// name. The returned value will be nil if fd is not a valid file
// descriptor. On Unix systems, if the file descriptor is in
// non-blocking mode, NewFile will attempt to return a pollable File
// (one for which the SetDeadline methods work).
//
// After passing it to NewFile, fd may become invalid under the same
// conditions described in the comments of the Fd method, and the same
// constraints apply.
func NewFile(fd uintptr, name string) *File

14.1. 发送

//@pkg/lisafs/sock.go
// writeTo writes the passed iovec to the UDS and donates any passed FDs.
func writeTo(sock *unet.Socket, iovec [][]byte, dataLen int, fds []int) error {
    w := sock.Writer(true)
    //这里的fds是可选的, 会做为control msg来发送
    if len(fds) > 0 {
        w.PackFDs(fds...)
    }
    for n := 0; n < dataLen; {
        cur, err := w.WriteVec(iovec)
        ...
    }
}

14.2. 接收

//@pkg/lisafs/sock.go
// readFrom fills the passed buffer with data from the socket. It also returns
// any donated FDs.
func readFrom(sock *unet.Socket, buf []byte, wantFDs uint8) ([]int, error) {
    r := sock.Reader(true)
    r.EnableFDs(int(wantFDs))
    n := len(buf)
    for got := 0; got < n; {
        cur, err := r.ReadVec([][]byte{buf[got:]})
        ...
    }
}

14.3. 发送2

//@pkg/urpc/urpc.go
// marshal sends the given FD and json struct.
func marshal(s *unet.Socket, v interface{}, fs []*os.File) error {
    // Marshal to a buffer.
    data, err := json.Marshal(v)
    if err != nil {
        log.Warningf("urpc: error marshalling %s: %s", fmt.Sprintf("%v", v), err.Error())
        return err
    }

    // Write to the socket.
    w := s.Writer(true)
    if fs != nil {
        var fds []int
        for _, f := range fs {
            fds = append(fds, int(f.Fd()))
        }
        w.PackFDs(fds...)
    }
    ...
}

14.4. 接收2

//@pkg/urpc/urpc.go
// unmarhsal receives an FD (optional) and unmarshals the given struct.
func unmarshal(s *unet.Socket, v interface{}) ([]*os.File, error) {
    // Receive a single byte.
    r := s.Reader(true)
    r.EnableFDs(maxFiles)
    firstByte := make([]byte, 1)

    // Extract any FDs that may be there.
    if _, err := r.ReadVec([][]byte{firstByte}); err != nil {
        return nil, err
    }
    fds, err := r.ExtractFDs()
    ...
}

15. 创建临时文件并mmap成结构体

15.1. memfd_create()系统调用

       #include <sys/memfd.h>

       int memfd_create(const char *name, unsigned int flags);

用tmpfs创建一个临时文件, 这个文件和通常文件系统没关系, 但可以支持所有文件操做. 可以用这个文件来共享内存:

  • 进程A调用memfd_create(), 返回fd
  • 进程B去打开/proc/<pid>/fd/<fd>(pid是进程A的pid, fd是A调用memfd_create()返回的fd号.), 打开后可以mmap, 就看到进程A一样的内容了.

15.2. gvisor中的使用场景

//RTMemoryStats是要被mmap的结构体
// RTMemoryStatsSize is the size of the RTMemoryStats struct.
var RTMemoryStatsSize = unsafe.Sizeof(RTMemoryStats{})

// RTMemoryStatsPointer casts addr to a RTMemoryStats pointer.
func RTMemoryStatsPointer(addr uintptr) *RTMemoryStats {
    return (*RTMemoryStats)(unsafe.Pointer(addr))
}


const name = "memory-usage"
fd, err := memutil.CreateMemFD(name, 0)
    p, err := unix.BytePtrFromString(name)
    fd, _, e := unix.Syscall(unix.SYS_MEMFD_CREATE, uintptr(unsafe.Pointer(p)), uintptr(flags), 0)
file := os.NewFile(uintptr(fd), name)
//设置文件大小为结构体RTMemoryStatsSize的大小
file.Truncate(int64(RTMemoryStatsSize))

//mmap这个文件
mmap, err := memutil.MapFile(0, RTMemoryStatsSize, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED, file.Fd(), 0)

//现在全局变量MemoryAccounting.RTMemoryStats就指向了这个文件.
//直接用那个结构体不香吗?
MemoryAccounting = &MemoryLocked{
    File:          file,
    RTMemoryStats: RTMemoryStatsPointer(mmap),
}

16. 用正则表达式 遍历/proc/self/maps

比如一个程序想解析当前进程的进程空间:

$ cat /proc/self/maps
55cb9cb6b000-55cb9cb73000 r-xp 00000000 fc:02 396                        /bin/cat
55cb9cd72000-55cb9cd73000 r--p 00007000 fc:02 396                        /bin/cat
55cb9cd73000-55cb9cd74000 rw-p 00008000 fc:02 396                        /bin/cat
55cb9e7a2000-55cb9e7c3000 rw-p 00000000 00:00 0                          [heap]
7f3367281000-7f336754d000 r--p 00000000 fc:02 7228                       /usr/lib/locale/locale-archive
7f336754d000-7f3367734000 r-xp 00000000 fc:02 44721                      /lib/x86_64-linux-gnu/libc-2.27.so
7f3367734000-7f3367934000 ---p 001e7000 fc:02 44721                      /lib/x86_64-linux-gnu/libc-2.27.so
7f3367934000-7f3367938000 r--p 001e7000 fc:02 44721                      /lib/x86_64-linux-gnu/libc-2.27.so
7f3367938000-7f336793a000 rw-p 001eb000 fc:02 44721                      /lib/x86_64-linux-gnu/libc-2.27.so
7f336793a000-7f336793e000 rw-p 00000000 00:00 0
7f336793e000-7f3367967000 r-xp 00000000 fc:02 44717                      /lib/x86_64-linux-gnu/ld-2.27.so
7f3367b33000-7f3367b57000 rw-p 00000000 00:00 0
7f3367b67000-7f3367b68000 r--p 00029000 fc:02 44717                      /lib/x86_64-linux-gnu/ld-2.27.so
7f3367b68000-7f3367b69000 rw-p 0002a000 fc:02 44717                      /lib/x86_64-linux-gnu/ld-2.27.so
7f3367b69000-7f3367b6a000 rw-p 00000000 00:00 0
7fff4ad74000-7fff4ad95000 rw-p 00000000 00:00 0                          [stack]
7fff4adda000-7fff4addd000 r--p 00000000 00:00 0                          [vvar]
7fff4addd000-7fff4addf000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

可以用regex

// mapsLine matches a single line from /proc/PID/maps.
var mapsLine = regexp.MustCompile("([0-9a-f]+)-([0-9a-f]+) ([r-][w-][x-][sp]) ([0-9a-f]+) [0-9a-f]{2,3}:[0-9a-f]{2,} [0-9]+\\s+(.*)")

func parseMaps() {
    f, err := os.Open("/proc/self/maps")
    r := bufio.NewReader(f)
    for {
        b, err := r.ReadBytes('\n')
        m := mapsLine.FindSubmatch(b)
        start, err := strconv.ParseUint(string(m[1]), 16, 64)
        end, err := strconv.ParseUint(string(m[2]), 16, 64)
        read := m[3][0] == 'r'
        write := m[3][1] == 'w'
        execute := m[3][2] == 'x'
        shared := m[3][3] == 's'
        offset, err := strconv.ParseUint(string(m[4]), 16, 64)
    }
}

17. 递归缩进打印error

type vmError struct {
    self     error
    children []error
}

func (vme vmError) Error() string {
    var b strings.Builder
    fmt.Fprintf(&b, "----\n")
    if vme.self != nil {
        fmt.Fprintf(&b, "%v\n", vme.self)
    }
    for _, err := range vme.children {
        for _, s := range strings.Split(err.Error(), "\n") {
            if s != "\tat -" {
                fmt.Fprintf(&b, "\t%s\n", s)
            }
        }
    }
    return b.String()
}

18. 读go micro

18.1. cmd

micro的入口命令, 只调用了一个函数 这里import中的的v2选择了v2的tag.

package main

import (
    "github.com/micro/micro/v2/cmd"
)

func main() {
    cmd.Init()
}

这里的Init原型很特别: cmd/cmd.go中,

// Init initialised the command line
func Init(options ...micro.Option) {
    Setup(cmd.App(), options...)

    cmd.Init(
        cmd.Name(name),
        cmd.Description(description),
        cmd.Version(buildVersion()),
    )
}

这里面的Optiontype Option func(*Options), 一个函数签名, 入参是*Options Options是个巨大的结构体, 包含了所有的micro的概念.

type Options struct {
    Auth      auth.Auth
    Broker    broker.Broker
    Cmd       cmd.Cmd
    Config    config.Config
    Client    client.Client
    Server    server.Server
    Store     store.Store
    Registry  registry.Registry
    Runtime   runtime.Runtime
    Transport transport.Transport
    Profile   profile.Profile

    // Before and After funcs
    BeforeStart []func() error
    BeforeStop  []func() error
    AfterStart  []func() error
    AfterStop   []func() error

    // Other options for implementations of the interface
    // can be stored in a context
    Context context.Context

    Signal bool
}

18.1.1. cmd.APP()

func App() *cli.App {
    //这里的DefaultCmd是个interface, 由newCmd实例化
    //即var DefaultCmd = newCmd()
    return DefaultCmd.App()
}

type cmd struct {
    opts Options
    app  *cli.App
}

//newCmd返回的是Cmd这个interface
func newCmd(opts ...Option) Cmd {
    //实际的cmd是上面的结构体
    cmd := new(cmd)
    cmd.opts = options
    cmd.app = cli.NewApp()
    //返回cmd实例, 但从外面看起来是Cmd interface
    return cmd
}

//综上, DefaultCmd.App()实际上就是
func (c *cmd) App() *cli.App {
    return c.app
}
//那么c.app实际上就是cli.NewApp()的实例化结果.

上面的cli.NewApp()github.com/micro/cli/v2, 它实际上是urfave/cli的fork.

18.1.2. 已经注册的cmd

cmd/cmd.go

    app.Commands = append(app.Commands, new.Commands()...)
    app.Commands = append(app.Commands, runtime.Commands(options...)...)
    app.Commands = append(app.Commands, store.Commands(options...)...)
    app.Commands = append(app.Commands, config.Commands(options...)...)
    app.Commands = append(app.Commands, api.Commands(options...)...)
    app.Commands = append(app.Commands, auth.Commands()...)
    app.Commands = append(app.Commands, bot.Commands()...)
    app.Commands = append(app.Commands, cli.Commands()...)
    app.Commands = append(app.Commands, broker.Commands(options...)...)
    app.Commands = append(app.Commands, health.Commands(options...)...)
    app.Commands = append(app.Commands, proxy.Commands(options...)...)
    app.Commands = append(app.Commands, router.Commands(options...)...)
    app.Commands = append(app.Commands, tunnel.Commands(options...)...)
    app.Commands = append(app.Commands, network.Commands(options...)...)
    app.Commands = append(app.Commands, registry.Commands(options...)...)
    app.Commands = append(app.Commands, debug.Commands(options...)...)
    app.Commands = append(app.Commands, server.Commands(options...)...)
    app.Commands = append(app.Commands, service.Commands(options...)...)
    app.Commands = append(app.Commands, build.Commands()...)
    app.Commands = append(app.Commands, web.Commands(options...)...)

18.2. cli相关的cmd

cli除了注册了自己的cmd, 还注册了call stream publish等命令. 而且cli.Commands()中, cli命令本身和所有的子命令都是同级的. 只是cli被安排在了第一个

func Commands() []*cli.Command {
    commands := []*cli.Command{
        {
            Name:   "cli",
            Usage:  "Run the interactive CLI",
            Action: Run,
        },
        {
            Name:   "call",
            Usage:  "Call a service e.g micro call greeter Say.Hello '{\"name\": \"John\"}",
            //注意这里的Print是个闭包函数, 返回callService的包装. 类似装饰器
            Action: Print(callService),
            Flags: []cli.Flag{
                &cli.StringFlag{
                    Name:    "address",
                    Usage:   "Set the address of the service instance to call",
                    EnvVars: []string{"MICRO_ADDRESS"},
                },
                &cli.StringFlag{
                    Name:    "output, o",
                    Usage:   "Set the output format; json (default), raw",
                    EnvVars: []string{"MICRO_OUTPUT"},
                },
                &cli.StringSliceFlag{
                    Name:    "metadata",
                    Usage:   "A list of key-value pairs to be forwarded as metadata",
                    EnvVars: []string{"MICRO_METADATA"},
                },
            },
        },
        ...
    }
}

cli命令对应的Action是Run. 这个Run是个典型的Read-Eval-Print-Loop, 基本上是

for {
    //先readline()
    args, err := r.Readline()
    //准备参数
    args = strings.TrimSpace(args)
    parts := strings.Split(args, " ")
    //找到cmd
    cmd, ok := commands[name]
    //执行cmd
    rsp, err := cmd.exec(c, parts[1:])
    println(string(rsp))
}

micro的交互式cli很简单, 就是在顶层有个循环来readline, 执行cmd. 没有其他功能.

18.2.1. cli子命令

这些子命令, 比如call, publish, 实际调用的是internal/command/cli/command.go中的

func CallService(c *cli.Context, args []string) ([]byte, error) {
    参数和返回值都通过json格式编码
    d := json.NewDecoder(strings.NewReader(req))
    ctx := callContext(c)
    creq := (*cmd.DefaultOptions().Client).NewRequest(service, endpoint, request, client.WithContentType("application/json"))

    //实际调用的是client的call接口, 这是个同步调用, 发req, 等rsp
    err = (*cmd.DefaultOptions().Client).Call(ctx, creq, &rsp, opts...)
    //等待rsp并打印结果.
}

18.2.2. client接口

client是micro的核心抽象, 接口定义如下:

// Client is the interface used to make requests to services.
// It supports Request/Response via Transport and Publishing via the Broker.
// It also supports bidirectional streaming of requests.
type Client interface {
    Init(...Option) error
    Options() Options
    NewMessage(topic string, msg interface{}, opts ...MessageOption) Message
    NewRequest(service, endpoint string, req interface{}, reqOpts ...RequestOption) Request
    Call(ctx context.Context, req Request, rsp interface{}, opts ...CallOption) error
    Stream(ctx context.Context, req Request, opts ...CallOption) (Stream, error)
    Publish(ctx context.Context, msg Message, opts ...PublishOption) error
    String() string
}

client支持Req/Rsp(通过Transport抽象), 支持Pub/Sub(通过Broker)抽象, 还支持stream模式.

19. pattern match

简单的*通配符匹配

func patternMatchs(str, pattern string) bool {
    if len(pattern) == 0 {
        return false
    }
    strs := strings.Split(pattern, "*")
    //fmt.Println(strs, len(strs))
    var pos, index int
    if index = strings.Index(str, strs[0]); index != 0 {
        return false
    }
    end := strs[len(strs)-1]
    if index = strings.LastIndex(str, end); index+len(end) != len(str) {
        return false
    }
    for i, substr := range strs {
        if i == 0 || i == len(strs)-1 || len(substr) == 0 {
            continue
        }
        index = strings.Index(str[pos:], substr)
        if index == -1 {
            return false
        }
        pos += index + len(substr)
    }
    return true
}

20. 读fs_linux.go

periph.io/x/periph@v3.6.2+incompatible/host/sysfs/fs_linux.go

21. 善用字符串库函数--strings.Join

比如解析或|关系, 普通的思路是自己组类似flag1|flag2|flag3的字符串. 实际上, 下面的代码片段先把flag1 flag2 flag3分别append到[]string切片, 最后用strings.Join(out, "|")加或|操作符.

func (e epollEvent) String() string {
    var out []string
    for _, b := range bitmaskString {
        if e&b.e != 0 {
            out = append(out, b.s)
            e &^= b.e
        }
    }
    if e != 0 {
        out = append(out, "0x"+strconv.FormatUint(uint64(e), 16))
    }
    if len(out) == 0 {
        out = []string{"0"}
    }
    return strings.Join(out, "|")
}

22. 切片的插入

这个切片是个二叉树的顺序表的表达, 和sort包的思路一致. 先找到r.Name的位置i, 然后用append一个nil的方式把切片扩一个位置出来, 然后用内置的copy函数把i后面的元素往后都挪一个位置; 最后把位置i的元素填上.

为了把位置i后面的元素都挪一个位置, 代价是把这后面的所有元素都copy一遍 这里的copy实际上是overlap的, copy的时候, 源为l[i:], 目的是l[i+1:], 目的比源向后移动一个位置, 而且目的的元素个数也少了一个. 注意, 如果按照普通的for循环式的copy思路, src和dst重叠时不能正常工作的. 有人讨论说golang的copy语义类似memmove

func insertRef(l []*Ref, r *Ref) []*Ref {
    n := r.Name
    i := search(len(l), func(i int) bool { return l[i].Name > n })
    l = append(l, nil)
    copy(l[i+1:], l[i:])
    l[i] = r
    return l
}

下面是对应的快排 注意在search函数里, 并不需要传入要查找的切片; search调用f的时候, f会比较切片元素, f会访问切片. 在本例中, f能访问insertRef函数的l []*Ref变量, 这也是一种闭包形式?

// search implements the same algorithm as sort.Search().
//
// It was extracted to to not depend on sort, which depends on reflect.
func search(n int, f func(int) bool) int {
    lo := 0
    for hi := n; lo < hi; {
        if i := int(uint(lo+hi) >> 1); !f(i) {
            lo = i + 1
        } else {
            hi = i
        }
    }
    return lo
}

23. 匿名函数执行

下面的例子中, 匿名行数在定义后就执行, 就像普通的代码块执行一样: 变量rerr都能直接被匿名函数引用到. 那为什么要用func(){}()这种形式呢? 首先, 匿名函数也是要函数栈的, 多了匿名函数性能上不会更好; 但是, 因为遍历全局的map byName byNumber 有锁操作, 那么使用匿名函数配合defer关键词来加锁和解锁, 代码更优雅. 如果不要这个匿名的壳子, 要么不用defer, 自己找地方加unlock; 还想用defer的话, defer会在r.Open()后执行. 估计这里的逻辑是: 一定要在r.Open()unlock

func Open(name string) (i2c.BusCloser, error) {
    var r *Ref
    var err error
    func() {
        mu.Lock()
        defer mu.Unlock()
        if len(byName) == 0 {
            err = errors.New("i2creg: no bus found; did you forget to call Init()?")
            return
        }
        if len(name) == 0 {
            r = getDefault()
            return
        }
        // Try by name, by alias, by number.
        if r = byName[name]; r == nil {
            if r = byAlias[name]; r == nil {
                if i, err2 := strconv.Atoi(name); err2 == nil {
                    r = byNumber[i]
                }
            }
        }
    }()
    if err != nil {
        return nil, err
    }
    if r == nil {
        return nil, errors.New("i2creg: can't open unknown bus: " + strconv.Quote(name))
    }
    return r.Open()
}

results matching ""

    No results matching ""