11-Go实现最小容器
学习目标
- 使用 Go 语言实现一个最小容器
- 掌握 syscall.SysProcAttr 的使用
- 理解父子进程通信机制
- 能够编译和运行容器程序
- 掌握容器代码的调试技巧
前置知识
- Go 语言基础
- Linux 系统调用
- 进程管理基础
- 容器基础原理
一、项目结构
1.1 目录结构
container/
├── go.mod
├── main.go
├── container.go
├── namespace.go
├── cgroup.go
└── README.md
1.2 go.mod 文件
module container
go 1.21
require (
golang.org/x/sys v0.15.0
)
二、核心实现
2.1 main.go - 主程序
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"syscall"
)
func main() {
var (
rootfs = flag.String("rootfs", "", "Root filesystem path")
cmd = flag.String("cmd", "/bin/sh", "Command to run in container")
args = flag.String("args", "", "Command arguments")
)
flag.Parse()
if *rootfs == "" {
log.Fatal("rootfs is required")
}
// 检查 rootfs 是否存在
if _, err := os.Stat(*rootfs); os.IsNotExist(err) {
log.Fatalf("rootfs does not exist: %s", *rootfs)
}
// 解析命令参数
command := *cmd
commandArgs := []string{}
if *args != "" {
commandArgs = append(commandArgs, *args)
}
// 创建容器
container := NewContainer(*rootfs, command, commandArgs)
// 运行容器
if err := container.Run(); err != nil {
log.Fatalf("Failed to run container: %v", err)
}
}
2.2 container.go - 容器核心
package main
import (
"fmt"
"os"
"os/exec"
"syscall"
"golang.org/x/sys/unix"
)
type Container struct {
Rootfs string
Cmd string
Args []string
}
func NewContainer(rootfs, cmd string, args []string) *Container {
return &Container{
Rootfs: rootfs,
Cmd: cmd,
Args: args,
}
}
func (c *Container) Run() error {
// 创建所有 namespace 的 flags
flags := syscall.CLONE_NEWUTS |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWCGROUP
// 准备子进程命令
cmd := exec.Command("/proc/self/exe", "child", c.Rootfs, c.Cmd)
cmd.Args = append(cmd.Args, c.Args...)
// 设置系统调用属性
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: flags,
Unshareflags: syscall.CLONE_NEWNS,
}
// 设置标准输入输出
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 启动子进程
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start child process: %v", err)
}
// 等待子进程完成
return cmd.Wait()
}
// 子进程入口点
func runChild() {
// 解析命令行参数
if len(os.Args) < 4 {
log.Fatal("child process requires rootfs, cmd, and args")
}
rootfs := os.Args[2]
cmd := os.Args[3]
args := os.Args[4:]
// 设置容器环境
if err := setupContainer(rootfs, cmd, args); err != nil {
log.Fatalf("Failed to setup container: %v", err)
}
}
func setupContainer(rootfs, cmd string, args []string) error {
// 1. 重新挂载 /proc
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to mount /proc: %v", err)
}
// 2. 重新挂载 /sys
if err := unix.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /sys: %v", err)
}
// 3. 重新挂载 /dev
if err := unix.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /dev: %v", err)
}
// 4. 创建必要设备文件
if err := createDevices(); err != nil {
return fmt.Errorf("failed to create devices: %v", err)
}
// 5. 切换根目录
if err := pivotRoot(rootfs); err != nil {
return fmt.Errorf("failed to pivot root: %v", err)
}
// 6. 重新挂载特殊文件系统
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to remount /proc: %v", err)
}
if err := unix.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
return fmt.Errorf("failed to remount /sys: %v", err)
}
if err := unix.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
return fmt.Errorf("failed to remount /dev: %v", err)
}
// 7. 设置主机名
if err := unix.Sethostname([]byte("container")); err != nil {
return fmt.Errorf("failed to set hostname: %v", err)
}
// 8. 执行命令
if err := syscall.Exec(cmd, append([]string{cmd}, args...), os.Environ()); err != nil {
return fmt.Errorf("failed to exec command: %v", err)
}
return nil
}
func pivotRoot(newroot string) error {
// 1. 绑定挂载 newroot
if err := unix.Mount(newroot, newroot, "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
return fmt.Errorf("failed to bind mount newroot: %v", err)
}
// 2. 创建 put_old 目录
putold := "/.oldroot"
if err := os.MkdirAll(putold, 0700); err != nil {
return fmt.Errorf("failed to create put_old directory: %v", err)
}
// 3. 执行 pivot_root
if err := unix.PivotRoot(newroot, putold); err != nil {
return fmt.Errorf("failed to pivot_root: %v", err)
}
// 4. 切换到新根目录
if err := os.Chdir("/"); err != nil {
return fmt.Errorf("failed to change working directory: %v", err)
}
// 5. 卸载原根目录
if err := unix.Unmount(putold, unix.MNT_DETACH); err != nil {
return fmt.Errorf("failed to unmount old root: %v", err)
}
// 6. 删除 put_old 目录
if err := os.RemoveAll(putold); err != nil {
return fmt.Errorf("failed to remove put_old directory: %v", err)
}
return nil
}
func createDevices() error {
devices := []struct {
name string
mode uint32
dev int
}{
{"/dev/null", syscall.S_IFCHR | 0666, 0x0103},
{"/dev/zero", syscall.S_IFCHR | 0666, 0x0105},
{"/dev/random", syscall.S_IFCHR | 0666, 0x0108},
{"/dev/urandom", syscall.S_IFCHR | 0666, 0x0109},
}
for _, device := range devices {
if err := unix.Mknod(device.name, device.mode, device.dev); err != nil {
return fmt.Errorf("failed to create device %s: %v", device.name, err)
}
}
return nil
}
2.3 namespace.go - 命名空间管理
package main
import (
"fmt"
"os"
"syscall"
"golang.org/x/sys/unix"
)
// NamespaceConfig 命名空间配置
type NamespaceConfig struct {
UTS bool // 主机名隔离
PID bool // 进程隔离
MNT bool // 文件系统隔离
NET bool // 网络隔离
IPC bool // 进程间通信隔离
USER bool // 用户隔离
CGROUP bool // 控制组隔离
}
// NewNamespaceConfig 创建默认命名空间配置
func NewNamespaceConfig() *NamespaceConfig {
return &NamespaceConfig{
UTS: true,
PID: true,
MNT: true,
NET: true,
IPC: true,
USER: true,
CGROUP: true,
}
}
// GetCloneFlags 获取 clone 标志
func (nc *NamespaceConfig) GetCloneFlags() uintptr {
var flags uintptr
if nc.UTS {
flags |= syscall.CLONE_NEWUTS
}
if nc.PID {
flags |= syscall.CLONE_NEWPID
}
if nc.MNT {
flags |= syscall.CLONE_NEWNS
}
if nc.NET {
flags |= syscall.CLONE_NEWNET
}
if nc.IPC {
flags |= syscall.CLONE_NEWIPC
}
if nc.USER {
flags |= syscall.CLONE_NEWUSER
}
if nc.CGROUP {
flags |= syscall.CLONE_NEWCGROUP
}
return flags
}
// SetupNamespaces 设置命名空间
func SetupNamespaces(config *NamespaceConfig) error {
// 设置 UTS namespace
if config.UTS {
if err := unix.Sethostname([]byte("container")); err != nil {
return fmt.Errorf("failed to set hostname: %v", err)
}
}
// 设置 PID namespace
if config.PID {
// 重新挂载 /proc
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to mount /proc: %v", err)
}
}
// 设置 MNT namespace
if config.MNT {
// 重新挂载特殊文件系统
if err := unix.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /sys: %v", err)
}
if err := unix.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /dev: %v", err)
}
}
// 设置 NET namespace
if config.NET {
// 启动 lo 接口
if err := unix.SetsockoptString(unix.AF_INET, unix.SOL_SOCKET, unix.SO_BINDTODEVICE, "lo"); err != nil {
return fmt.Errorf("failed to bind to lo: %v", err)
}
}
return nil
}
// GetCurrentNamespaces 获取当前命名空间
func GetCurrentNamespaces() (map[string]string, error) {
namespaces := make(map[string]string)
nsTypes := []string{"uts", "pid", "mnt", "net", "ipc", "user", "cgroup"}
for _, nsType := range nsTypes {
nsPath := fmt.Sprintf("/proc/self/ns/%s", nsType)
if target, err := os.Readlink(nsPath); err == nil {
namespaces[nsType] = target
}
}
return namespaces, nil
}
// PrintNamespaces 打印命名空间信息
func PrintNamespaces() error {
namespaces, err := GetCurrentNamespaces()
if err != nil {
return err
}
fmt.Println("=== 当前命名空间 ===")
for nsType, nsPath := range namespaces {
fmt.Printf("%s: %s\n", nsType, nsPath)
}
return nil
}
2.4 cgroup.go - 控制组管理
package main
import (
"fmt"
"os"
"path/filepath"
"strconv"
)
// CGroupConfig 控制组配置
type CGroupConfig struct {
MemoryMax string // 内存限制,如 "128M"
CPUMax string // CPU 限制,如 "50000 100000"
PidsMax int // 进程数限制
}
// NewCGroupConfig 创建默认控制组配置
func NewCGroupConfig() *CGroupConfig {
return &CGroupConfig{
MemoryMax: "128M",
CPUMax: "50000 100000",
PidsMax: 100,
}
}
// CGroupManager 控制组管理器
type CGroupManager struct {
config *CGroupConfig
path string
}
// NewCGroupManager 创建控制组管理器
func NewCGroupManager(config *CGroupConfig, containerID string) *CGroupManager {
return &CGroupManager{
config: config,
path: filepath.Join("/sys/fs/cgroup", containerID),
}
}
// Create 创建控制组
func (cgm *CGroupManager) Create() error {
// 创建控制组目录
if err := os.MkdirAll(cgm.path, 0755); err != nil {
return fmt.Errorf("failed to create cgroup directory: %v", err)
}
// 设置内存限制
if err := cgm.setMemoryLimit(); err != nil {
return fmt.Errorf("failed to set memory limit: %v", err)
}
// 设置 CPU 限制
if err := cgm.setCPULimit(); err != nil {
return fmt.Errorf("failed to set CPU limit: %v", err)
}
// 设置进程数限制
if err := cgm.setPidsLimit(); err != nil {
return fmt.Errorf("failed to set pids limit: %v", err)
}
return nil
}
// AddProcess 添加进程到控制组
func (cgm *CGroupManager) AddProcess(pid int) error {
cgroupProcsPath := filepath.Join(cgm.path, "cgroup.procs")
return os.WriteFile(cgroupProcsPath, []byte(strconv.Itoa(pid)), 0644)
}
// Destroy 销毁控制组
func (cgm *CGroupManager) Destroy() error {
return os.RemoveAll(cgm.path)
}
// setMemoryLimit 设置内存限制
func (cgm *CGroupManager) setMemoryLimit() error {
memoryMaxPath := filepath.Join(cgm.path, "memory.max")
return os.WriteFile(memoryMaxPath, []byte(cgm.config.MemoryMax), 0644)
}
// setCPULimit 设置 CPU 限制
func (cgm *CGroupManager) setCPULimit() error {
cpuMaxPath := filepath.Join(cgm.path, "cpu.max")
return os.WriteFile(cpuMaxPath, []byte(cgm.config.CPUMax), 0644)
}
// setPidsLimit 设置进程数限制
func (cgm *CGroupManager) setPidsLimit() error {
pidsMaxPath := filepath.Join(cgm.path, "pids.max")
return os.WriteFile(pidsMaxPath, []byte(strconv.Itoa(cgm.config.PidsMax)), 0644)
}
// GetStats 获取控制组统计信息
func (cgm *CGroupManager) GetStats() (map[string]string, error) {
stats := make(map[string]string)
// 读取内存使用情况
if data, err := os.ReadFile(filepath.Join(cgm.path, "memory.current")); err == nil {
stats["memory.current"] = string(data)
}
// 读取 CPU 使用情况
if data, err := os.ReadFile(filepath.Join(cgm.path, "cpu.stat")); err == nil {
stats["cpu.stat"] = string(data)
}
// 读取进程数
if data, err := os.ReadFile(filepath.Join(cgm.path, "pids.current")); err == nil {
stats["pids.current"] = string(data)
}
return stats, nil
}
// PrintStats 打印控制组统计信息
func (cgm *CGroupManager) PrintStats() error {
stats, err := cgm.GetStats()
if err != nil {
return err
}
fmt.Println("=== 控制组统计信息 ===")
for key, value := range stats {
fmt.Printf("%s: %s", key, value)
}
return nil
}
三、完整实现
3.1 完整的 main.go
package main
import (
"flag"
"fmt"
"log"
"os"
"os/exec"
"syscall"
"golang.org/x/sys/unix"
)
func main() {
var (
rootfs = flag.String("rootfs", "", "Root filesystem path")
cmd = flag.String("cmd", "/bin/sh", "Command to run in container")
args = flag.String("args", "", "Command arguments")
memory = flag.String("memory", "128M", "Memory limit")
cpu = flag.String("cpu", "50000 100000", "CPU limit")
pids = flag.Int("pids", 100, "Process limit")
)
flag.Parse()
if *rootfs == "" {
log.Fatal("rootfs is required")
}
// 检查 rootfs 是否存在
if _, err := os.Stat(*rootfs); os.IsNotExist(err) {
log.Fatalf("rootfs does not exist: %s", *rootfs)
}
// 解析命令参数
command := *cmd
commandArgs := []string{}
if *args != "" {
commandArgs = append(commandArgs, *args)
}
// 创建容器
container := NewContainer(*rootfs, command, commandArgs)
// 设置资源限制
container.SetResourceLimits(*memory, *cpu, *pids)
// 运行容器
if err := container.Run(); err != nil {
log.Fatalf("Failed to run container: %v", err)
}
}
type Container struct {
Rootfs string
Cmd string
Args []string
Memory string
CPU string
Pids int
}
func NewContainer(rootfs, cmd string, args []string) *Container {
return &Container{
Rootfs: rootfs,
Cmd: cmd,
Args: args,
}
}
func (c *Container) SetResourceLimits(memory, cpu string, pids int) {
c.Memory = memory
c.CPU = cpu
c.Pids = pids
}
func (c *Container) Run() error {
// 创建所有 namespace 的 flags
flags := syscall.CLONE_NEWUTS |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNS |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWUSER |
syscall.CLONE_NEWCGROUP
// 准备子进程命令
cmd := exec.Command("/proc/self/exe", "child", c.Rootfs, c.Cmd)
cmd.Args = append(cmd.Args, c.Args...)
// 设置系统调用属性
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: flags,
Unshareflags: syscall.CLONE_NEWNS,
}
// 设置标准输入输出
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// 启动子进程
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start child process: %v", err)
}
// 创建控制组
cgroupConfig := &CGroupConfig{
MemoryMax: c.Memory,
CPUMax: c.CPU,
PidsMax: c.Pids,
}
cgroupManager := NewCGroupManager(cgroupConfig, fmt.Sprintf("container-%d", cmd.Process.Pid))
if err := cgroupManager.Create(); err != nil {
return fmt.Errorf("failed to create cgroup: %v", err)
}
// 将进程添加到控制组
if err := cgroupManager.AddProcess(cmd.Process.Pid); err != nil {
return fmt.Errorf("failed to add process to cgroup: %v", err)
}
// 等待子进程完成
return cmd.Wait()
}
// 子进程入口点
func runChild() {
// 解析命令行参数
if len(os.Args) < 4 {
log.Fatal("child process requires rootfs, cmd, and args")
}
rootfs := os.Args[2]
cmd := os.Args[3]
args := os.Args[4:]
// 设置容器环境
if err := setupContainer(rootfs, cmd, args); err != nil {
log.Fatalf("Failed to setup container: %v", err)
}
}
func setupContainer(rootfs, cmd string, args []string) error {
// 1. 重新挂载 /proc
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to mount /proc: %v", err)
}
// 2. 重新挂载 /sys
if err := unix.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /sys: %v", err)
}
// 3. 重新挂载 /dev
if err := unix.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
return fmt.Errorf("failed to mount /dev: %v", err)
}
// 4. 创建必要设备文件
if err := createDevices(); err != nil {
return fmt.Errorf("failed to create devices: %v", err)
}
// 5. 切换根目录
if err := pivotRoot(rootfs); err != nil {
return fmt.Errorf("failed to pivot root: %v", err)
}
// 6. 重新挂载特殊文件系统
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to remount /proc: %v", err)
}
if err := unix.Mount("sysfs", "/sys", "sysfs", 0, ""); err != nil {
return fmt.Errorf("failed to remount /sys: %v", err)
}
if err := unix.Mount("devtmpfs", "/dev", "devtmpfs", 0, ""); err != nil {
return fmt.Errorf("failed to remount /dev: %v", err)
}
// 7. 设置主机名
if err := unix.Sethostname([]byte("container")); err != nil {
return fmt.Errorf("failed to set hostname: %v", err)
}
// 8. 执行命令
if err := syscall.Exec(cmd, append([]string{cmd}, args...), os.Environ()); err != nil {
return fmt.Errorf("failed to exec command: %v", err)
}
return nil
}
func pivotRoot(newroot string) error {
// 1. 绑定挂载 newroot
if err := unix.Mount(newroot, newroot, "", unix.MS_BIND|unix.MS_REC, ""); err != nil {
return fmt.Errorf("failed to bind mount newroot: %v", err)
}
// 2. 创建 put_old 目录
putold := "/.oldroot"
if err := os.MkdirAll(putold, 0700); err != nil {
return fmt.Errorf("failed to create put_old directory: %v", err)
}
// 3. 执行 pivot_root
if err := unix.PivotRoot(newroot, putold); err != nil {
return fmt.Errorf("failed to pivot_root: %v", err)
}
// 4. 切换到新根目录
if err := os.Chdir("/"); err != nil {
return fmt.Errorf("failed to change working directory: %v", err)
}
// 5. 卸载原根目录
if err := unix.Unmount(putold, unix.MNT_DETACH); err != nil {
return fmt.Errorf("failed to unmount old root: %v", err)
}
// 6. 删除 put_old 目录
if err := os.RemoveAll(putold); err != nil {
return fmt.Errorf("failed to remove put_old directory: %v", err)
}
return nil
}
func createDevices() error {
devices := []struct {
name string
mode uint32
dev int
}{
{"/dev/null", syscall.S_IFCHR | 0666, 0x0103},
{"/dev/zero", syscall.S_IFCHR | 0666, 0x0105},
{"/dev/random", syscall.S_IFCHR | 0666, 0x0108},
{"/dev/urandom", syscall.S_IFCHR | 0666, 0x0109},
}
for _, device := range devices {
if err := unix.Mknod(device.name, device.mode, device.dev); err != nil {
return fmt.Errorf("failed to create device %s: %v", device.name, err)
}
}
return nil
}
四、编译和运行
4.1 编译程序
# 1. 初始化 Go 模块
go mod init container
# 2. 下载依赖
go mod tidy
# 3. 编译程序
go build -o container main.go
# 4. 检查编译结果
ls -la container
4.2 准备 RootFS
#!/bin/bash
# 准备 RootFS
ROOTFS="/tmp/container-rootfs"
echo "=== 准备 RootFS: $ROOTFS ==="
# 1. 创建目录结构
mkdir -p $ROOTFS/{bin,usr,etc,proc,sys,dev,tmp,var,home,root}
# 2. 下载并安装 busybox
if [ ! -f "$ROOTFS/busybox" ]; then
wget -q https://busybox.net/downloads/binaries/1.31.0-defconfig-multiarch-musl/busybox-x86_64 -O $ROOTFS/busybox
chmod +x $ROOTFS/busybox
fi
cd $ROOTFS
./busybox --install -s .
# 3. 创建系统文件
cat > etc/passwd << 'EOF'
root:x:0:0:root:/root:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/false
EOF
cat > etc/group << 'EOF'
root:x:0:
nogroup:x:65534:
EOF
echo "container" > etc/hostname
echo "127.0.0.1 localhost" > etc/hosts
echo "nameserver 8.8.8.8" > etc/resolv.conf
chmod 644 etc/passwd etc/group etc/hostname etc/hosts etc/resolv.conf
chmod 755 bin usr etc proc sys dev tmp var home root
chmod 1777 tmp
echo "RootFS 准备完成"
4.3 运行容器
# 1. 运行容器
sudo ./container -rootfs /tmp/container-rootfs -cmd /bin/sh
# 2. 运行容器并指定参数
sudo ./container -rootfs /tmp/container-rootfs -cmd /bin/ls -args "-la /"
# 3. 运行容器并设置资源限制
sudo ./container -rootfs /tmp/container-rootfs -cmd /bin/sh -memory 256M -cpu "100000 100000" -pids 50
五、调试技巧
5.1 调试模式
// 添加调试模式
var debug = flag.Bool("debug", false, "Enable debug mode")
func (c *Container) Run() error {
if *debug {
fmt.Printf("Starting container with rootfs: %s\n", c.Rootfs)
fmt.Printf("Command: %s %v\n", c.Cmd, c.Args)
}
// ... 其他代码
}
5.2 日志记录
import "log"
func setupContainer(rootfs, cmd string, args []string) error {
log.Printf("Setting up container with rootfs: %s", rootfs)
// 1. 重新挂载 /proc
log.Printf("Mounting /proc")
if err := unix.Mount("proc", "/proc", "proc", 0, ""); err != nil {
return fmt.Errorf("failed to mount /proc: %v", err)
}
// ... 其他代码
}
5.3 错误处理
func (c *Container) Run() error {
// 启动子进程
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start child process: %v", err)
}
// 等待子进程完成
if err := cmd.Wait(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return fmt.Errorf("child process exited with code %d: %v", exitError.ExitCode(), err)
}
return fmt.Errorf("failed to wait for child process: %v", err)
}
return nil
}
六、验证检查清单
基础功能
- [ ] 能够编译 Go 程序
- [ ] 能够准备 RootFS
- [ ] 能够运行容器
- [ ] 能够验证容器隔离
代码理解
- [ ] 理解 syscall.SysProcAttr 的使用
- [ ] 理解父子进程通信机制
- [ ] 理解 pivot_root 的实现
- [ ] 理解控制组的配置
调试技能
- [ ] 能够使用调试模式
- [ ] 能够添加日志记录
- [ ] 能够处理错误
- [ ] 能够进行性能测试
相关链接
- 10-命令行手撸容器 - 命令行实践
- 12-Go实现完整容器 - 完整功能实现
- 14-调试技术与工具 - 调试技术详解
下一步:让我们学习 Go 实现完整容器,这是功能完整的实现!