Go Engineering System Course 015

Docker Containerization — A Practical Guide for Go Projects


I. Docker Core Concepts

1.1 What is Docker

Docker is an open-source containerization platform that packages applications and all their dependencies into a standardized unit (container), achieving "build once, run anywhere." For Go developers, Docker addresses the following pain points:

  • Inconsistent development and production environments
  • Complex dependency management (databases, caches, message queues, and other middleware)
  • Non-standardized deployment processes
  • Difficulty in multi-service collaboration in a microservices architecture

1.2 Three Core Concepts

Image

An image is a read-only template that contains all the file system layers required to run an application. It can be compared to a "Class":

  • An image consists of multiple layers, each representing an instruction in the Dockerfile
  • Layers are read-only and reusable; multiple images can share underlying layers
  • Images are version-managed using tag, e.g., golang:1.22-alpine
+---------------------------+
|   应用代码层 (COPY .)      |  <-- 最上层,变化最频繁
+---------------------------+
|   依赖安装层 (go mod)      |
+---------------------------+
|   基础工具层 (RUN apk)     |
+---------------------------+
|   基础镜像层 (alpine)      |  <-- 最底层,最稳定
+---------------------------+

Container

A container is a running instance of an image, comparable to an "Object":

  • A container adds a writable layer on top of an image
  • Each container has its own independent file system, network, and process space
  • Containers are temporary and stateless; data in the writable layer is lost upon destruction
  • Data requiring persistence should use data volumes (Volume)

Registry

A registry is a service for storing and distributing images:

  • Docker Hub: The official public registry, similar to GitHub
  • Private Registries: Such as Harbor, AWS ECR, Alibaba Cloud ACR
  • Images are identified by the format registry/repository:tag, e.g., docker.io/library/golang:1.22

II. Writing a Dockerfile (Go Project Example)

2.1 Basic Dockerfile

Suppose we have a simple Go Web service:

// main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        fmt.Fprintf(w, "OK")
    })

    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go Docker Service!")
    })

    log.Printf("Server starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}

The simplest but not recommended Dockerfile:

# 不推荐:镜像体积大,包含编译工具链
FROM golang:1.22

WORKDIR /app
COPY . .
RUN go build -o server .

EXPOSE 8080
CMD ["./server"]

Images generated this way typically exceed 800MB because they include the complete Go toolchain.

2.2 Multi-stage Build

Multi-stage builds are a core technique for Dockerizing Go projects, capable of reducing image size from 800MB+ to 10-20MB:

# ============ 第一阶段:编译 ============
FROM golang:1.22-alpine AS builder

# 设置 Go 模块代理(国内加速)
ENV GOPROXY=https://goproxy.cn,direct

WORKDIR /app

# 先复制依赖文件,利用缓存层(详见 2.3 节)
COPY go.mod go.sum ./
RUN go mod download

# 复制源代码并编译
COPY . .

# CGO_ENABLED=0 生成静态链接的二进制文件
# -ldflags="-s -w" 去除调试信息,减小体积
RUN CGO_ENABLED=0 GOOS=linux go build \
    -ldflags="-s -w" \
    -o /app/server .

# ============ 第二阶段:运行 ============
FROM alpine:3.19

# 安装必要的 CA 证书(HTTPS 请求需要)和时区数据
RUN apk --no-cache add ca-certificates tzdata

# 创建非 root 用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

WORKDIR /app

# 从 builder 阶段复制编译好的二进制文件
COPY --from=builder /app/server .

# 复制配置文件(如果有)
# COPY --from=builder /app/config ./config

# 切换到非 root 用户
USER appuser

EXPOSE 8080

# 使用 ENTRYPOINT 而非 CMD,防止被意外覆盖
ENTRYPOINT ["./server"]

More extreme solution -- using a scratch empty image:

# 编译阶段同上...

# 使用空镜像,最终镜像仅包含二进制文件
FROM scratch

# 从 builder 复制 CA 证书
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 从 builder 复制时区信息
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo

COPY --from=builder /app/server /server

EXPOSE 8080
ENTRYPOINT ["/server"]

Image size comparison:

Base Image Final Size Use Case
golang:1.22 ~850MB Not recommended for production
alpine:3.19 ~15MB Recommended, supports shell debugging
scratch ~8MB Extremely minimal, no shell
distroless ~12MB Google recommended, good security

2.3 Build Cache Optimization

During Docker builds, each layer is cached. As long as the input for a layer hasn't changed, the cache will be reused. The key principle is: place layers with low change frequency first, and layers with high change frequency later.

# 好的做法:分离依赖下载和代码编译
COPY go.mod go.sum ./       # 依赖文件变化少,缓存命中率高
RUN go mod download          # 只有依赖变化时才重新下载
COPY . .                     # 代码频繁变化
RUN go build -o server .     # 每次代码变化都重新编译

# 坏的做法:一次性复制所有文件
COPY . .                     # 任何文件变化都导致后续所有层缓存失效
RUN go mod download
RUN go build -o server .

2.4 Best Practices Summary

.dockerignore File

Create a .dockerignore file in the project root to reduce build context size:

# .dockerignore
.git
.gitignore
.idea
.vscode
*.md
README*
LICENSE
Makefile
docker-compose*.yml
Dockerfile*
tmp/
vendor/
bin/
*.test
*.prof

Security Best Practices

# 1. 使用特定版本标签,避免用 latest
FROM alpine:3.19    # 好
FROM alpine:latest  # 坏 -- 不可复现

# 2. 以非 root 用户运行
RUN addgroup -S app && adduser -S app -G app
USER app

# 3. 只读文件系统(运行时指定)
# docker run --read-only --tmpfs /tmp myapp

# 4. 不要在镜像中存储密钥
# 坏的做法
ENV DB_PASSWORD=secret123
# 好的做法:运行时通过环境变量或 Secret 管理工具注入

III. Docker Compose Orchestration

3.1 Why Docker Compose is Needed

In a microservices architecture, a Go project typically depends on multiple external services. Docker Compose allows defining and managing the orchestration of multiple containers using a single YAML file.

3.2 Complete Example: Go Service + MySQL + Redis + Consul

# docker-compose.yml
version: "3.9"

services:
  # ============ Go 应用服务 ============
  app:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        - GOPROXY=https://goproxy.cn,direct
    container_name: go-app
    ports:
      - "8080:8080"    # 主机端口:容器端口
    environment:
      - PORT=8080
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=rootpassword
      - DB_NAME=myapp
      - REDIS_ADDR=redis:6379
      - CONSUL_ADDR=consul:8500
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      consul:
        condition: service_started
    networks:
      - app-network
    restart: unless-stopped
    # 资源限制
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M

  # ============ MySQL 数据库 ============
  mysql:
    image: mysql:8.0
    container_name: go-mysql
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: myapp
      MYSQL_CHARSET: utf8mb4
    volumes:
      - mysql-data:/var/lib/mysql              # 数据持久化
      - ./deploy/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql  # 初始化脚本
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ============ Redis 缓存 ============
  redis:
    image: redis:7-alpine
    container_name: go-redis
    ports:
      - "6379:6379"
    command: redis-server --requirepass redispassword --appendonly yes
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "redispassword", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - app-network
    restart: unless-stopped

  # ============ Consul 服务注册与发现 ============
  consul:
    image: hashicorp/consul:1.17
    container_name: go-consul
    ports:
      - "8500:8500"    # HTTP API 和 Web UI
      - "8600:8600/udp" # DNS
    command: agent -server -bootstrap-expect=1 -ui -client=0.0.0.0
    volumes:
      - consul-data:/consul/data
    networks:
      - app-network
    restart: unless-stopped

# ============ 数据卷定义 ============
volumes:
  mysql-data:
    driver: local
  redis-data:
    driver: local
  consul-data:
    driver: local

# ============ 网络定义 ============
networks:
  app-network:
    driver: bridge

3.3 Common Docker Compose Commands

# 启动所有服务(后台运行)
docker-compose up -d

# 启动并重新构建镜像
docker-compose up -d --build

# 查看服务状态
docker-compose ps

# 查看某个服务的日志
docker-compose logs -f app

# 停止所有服务
docker-compose down

# 停止并删除数据卷(慎用,会丢失数据)
docker-compose down -v

# 只启动某个服务及其依赖
docker-compose up -d app

# 进入某个服务的容器
docker-compose exec app sh

# 扩展服务实例数
docker-compose up -d --scale app=3

IV. Quick Reference for Common Docker Commands

4.1 Image Management

# 构建镜像
docker build -t myapp:v1.0 .
docker build -t myapp:v1.0 -f deploy/Dockerfile .   # 指定 Dockerfile 路径

# 查看本地镜像
docker images
docker image ls

# 删除镜像
docker rmi myapp:v1.0
docker image prune           # 清理悬空镜像(dangling images)
docker image prune -a        # 清理所有未使用的镜像

# 镜像标签
docker tag myapp:v1.0 registry.example.com/myapp:v1.0

# 导入导出(离线场景)
docker save -o myapp.tar myapp:v1.0
docker load -i myapp.tar

# 查看镜像构建历史 / 层信息
docker history myapp:v1.0
docker inspect myapp:v1.0

4.2 Container Management

# 运行容器
docker run -d --name myapp -p 8080:8080 myapp:v1.0
docker run -d --name myapp \
  -p 8080:8080 \
  -e DB_HOST=localhost \
  -v /host/data:/app/data \
  --restart unless-stopped \
  myapp:v1.0

# 查看运行中的容器
docker ps
docker ps -a                 # 包含已停止的容器

# 容器生命周期
docker start myapp
docker stop myapp
docker restart myapp
docker rm myapp              # 删除已停止的容器
docker rm -f myapp           # 强制删除(包括运行中的)

# 进入容器内部
docker exec -it myapp sh     # alpine 用 sh
docker exec -it myapp bash   # 带 bash 的镜像用 bash

# 查看日志
docker logs myapp
docker logs -f myapp         # 实时跟踪
docker logs --tail 100 myapp # 最后100行

# 查看容器资源使用
docker stats
docker stats myapp

# 复制文件
docker cp myapp:/app/logs/app.log ./app.log   # 容器到主机
docker cp ./config.yaml myapp:/app/config.yaml # 主机到容器

4.3 System Cleanup

# 一键清理:停止的容器、悬空镜像、未使用的网络
docker system prune

# 包含未使用的数据卷(慎用)
docker system prune --volumes

# 查看 Docker 磁盘使用情况
docker system df

V. Docker Network Modes

5.1 Bridge Mode (Default)

Each container has its own independent network namespace and communicates via the virtual bridge docker0:

# 创建自定义 bridge 网络
docker network create my-network

# 将容器连接到自定义网络
docker run -d --name app --network my-network myapp:v1.0
docker run -d --name db --network my-network mysql:8.0

# 同一网络内的容器可以通过容器名互相访问
# app 容器内可以用 "db:3306" 连接数据库

Advantages of custom bridge networks:
- Containers discover each other by name (DNS)
- Better network isolation
- Ability to dynamically connect/disconnect containers

5.2 Host Mode

Containers directly use the host's network, with no network isolation:

docker run -d --network host myapp:v1.0
# 容器中监听 8080 端口 = 宿主机 8080 端口
# 不需要 -p 端口映射
  • Pros: Best performance, no NAT overhead
  • Cons: Port conflict risk, lower security
  • Use cases: High-performance network requirements, network debugging

5.3 None Mode

Containers have no network connection, completely isolated:

docker run -d --network none myapp:v1.0
  • Use cases: Tasks that only require computation and no network access

5.4 Network Mode Comparison

Mode Isolation Performance Use Case
bridge High Medium Default choice, most scenarios
host None High High-performance requirements
none Complete - Security-sensitive computational tasks
overlay High Medium Cross-host Swarm/K8s

VI. Data Volumes and Persistence

6.1 Why Data Volumes are Needed

A container's file system is temporary, and data is lost when the container is deleted. Data volumes provide a persistent storage mechanism.

6.2 Three Mount Types

# 1. 命名卷(Named Volume)-- 推荐用于数据持久化
docker volume create mydata
docker run -v mydata:/app/data myapp:v1.0

# 2. 绑定挂载(Bind Mount)-- 适合开发时挂载源代码
docker run -v /host/path:/container/path myapp:v1.0
docker run -v $(pwd)/config:/app/config:ro myapp:v1.0   # 只读挂载

# 3. tmpfs 挂载 -- 内存中的临时文件系统
docker run --tmpfs /app/tmp myapp:v1.0

6.3 Data Volume Management

# 查看所有卷
docker volume ls

# 查看卷详情
docker volume inspect mydata

# 删除卷
docker volume rm mydata

# 清理未使用的卷
docker volume prune

6.4 Typical Usage in Go Projects

# docker-compose.yml 中的卷使用
services:
  app:
    volumes:
      - ./config:/app/config:ro    # 配置文件(只读绑定)
      - app-logs:/app/logs         # 日志目录(命名卷)
      - /app/tmp                   # 匿名卷(临时文件)

  mysql:
    volumes:
      - mysql-data:/var/lib/mysql  # 数据库数据(命名卷,最重要)

volumes:
  app-logs:
  mysql-data:

VII. Docker's Application in Microservices Development

7.1 Local Development Workflow

In a microservices architecture, a Go service may depend on multiple other services. Docker makes local development easy:

+-----------------------------------------------+
|           docker-compose Orchestration        |
|                                               |
|  +----------+  +----------+  +----------+    |
|  | User Service |  | Order Service |  | Product Service |    |
|  | Go :8081 |  | Go :8082 |  | Go :8083 |    |
|  +----+-----+  +----+-----+  +----+-----+    |
|       |              |              |          |
|  +----+--------------+--------------+----+    |
|  |          app-network (bridge)         |    |
|  +----+----------+----------+-----------+    |
|       |          |          |                 |
|  +----+---+ +---+----+ +--+------+           |
|  | MySQL  | | Redis  | | Consul  |           |
|  | :3306  | | :6379  | | :8500   |           |
|  +--------+ +--------+ +---------+           |
+-----------------------------------------------+

7.2 Hot Reload Development Mode

During development, tools like air can be combined to achieve code hot reloading:

# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app                  # 挂载源代码
    command: air                 # 使用 air 热重载
    ports:
      - "8080:8080"
# Dockerfile.dev
FROM golang:1.22-alpine

RUN go install github.com/air-verse/air@latest

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

# 不需要 COPY . . 因为源代码通过 volume 挂载
CMD ["air"]

7.3 Integration Testing

Docker can easily set up an integration testing environment:

# 启动测试依赖
docker-compose -f docker-compose.test.yml up -d

# 运行集成测试
go test ./... -tags=integration

# 清理
docker-compose -f docker-compose.test.yml down -v

You can also dynamically create test containers in Go code using the testcontainers-go library:

package repository_test

import (
    "context"
    "testing"

    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/mysql"
)

func TestUserRepository(t *testing.T) {
    ctx := context.Background()

    // 动态创建 MySQL 容器用于测试
    mysqlC, err := mysql.Run(ctx,
        "mysql:8.0",
        mysql.WithDatabase("testdb"),
        mysql.WithUsername("test"),
        mysql.WithPassword("test"),
    )
    if err != nil {
        t.Fatal(err)
    }
    defer mysqlC.Terminate(ctx)

    // 获取连接地址
    host, _ := mysqlC.Host(ctx)
    port, _ := mysqlC.MappedPort(ctx, "3306")

    // 使用真实数据库进行测试...
    t.Logf("MySQL running at %s:%s", host, port.Port())
}

VIII. Pushing Images to Registry

8.1 Pushing to Docker Hub

# 1. Login
docker login

# 2. 给镜像打标签(格式:用户名/仓库名:标签)
docker tag myapp:v1.0 username/myapp:v1.0
docker tag myapp:v1.0 username/myapp:latest

# 3. 推送
docker push username/myapp:v1.0
docker push username/myapp:latest

8.2 Pushing to a Private Registry

# 以阿里云 ACR 为例
# 1. Login私有仓库
docker login registry.cn-hangzhou.aliyuncs.com

# 2. 打标签
docker tag myapp:v1.0 registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0

# 3. 推送
docker push registry.cn-hangzhou.aliyuncs.com/myns/myapp:v1.0

8.3 Setting up a Local Private Registry (Harbor)

Harbor is an enterprise-grade Docker image registry that supports features like access control and image scanning:

# 使用 Docker Compose 快速部署 Harbor
# 下载离线安装包后:
./install.sh --with-chartmuseum --with-trivy

# 推送到 Harbor
docker tag myapp:v1.0 harbor.example.com/myproject/myapp:v1.0
docker push harbor.example.com/myproject/myapp:v1.0

8.4 Automated Build and Push in CI/CD

Automatically build and push images in GitHub Actions:

# .github/workflows/docker.yml
name: Build and Push Docker Image

on:
  push:
    tags:
      - 'v*'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            username/myapp:${{ github.ref_name }}
            username/myapp:latest

IX. Complete Practical Workflow for Dockerizing Go Projects

9.1 Project Structure

myapp/
├── cmd/
│   └── server/
│       └── main.go          # Entrypoint
├── internal/
│   ├── handler/             # HTTP Handlers
│   ├── service/             # Business Logic
│   ├── repository/          # Data Access
│   └── config/              # Configuration Management
├── deploy/
│   ├── mysql/
│   │   └── init.sql         # Database Initialization
│   └── nginx/
│       └── nginx.conf       # Reverse Proxy Configuration
├── go.mod
├── go.sum
├── Dockerfile
├── docker-compose.yml
├── docker-compose.dev.yml
├── .dockerignore
└── Makefile

9.2 Makefile Automation

# Makefile
APP_NAME=myapp
VERSION=$(shell git describe --tags --always --dirty)
REGISTRY=registry.example.com/myns

.PHONY: build run test docker-build docker-push deploy clean

# 本地编译
build:
    CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(VERSION)" \
        -o bin/$(APP_NAME) ./cmd/server

# 本地运行
run:
    go run ./cmd/server

# 运行测试
test:
    go test ./... -v -cover

# 构建 Docker 镜像
docker-build:
    docker build -t $(APP_NAME):$(VERSION) .
    docker tag $(APP_NAME):$(VERSION) $(APP_NAME):latest

# 推送镜像
docker-push: docker-build
    docker tag $(APP_NAME):$(VERSION) $(REGISTRY)/$(APP_NAME):$(VERSION)
    docker push $(REGISTRY)/$(APP_NAME):$(VERSION)

# 启动开发环境
dev:
    docker-compose -f docker-compose.dev.yml up -d --build

# 启动生产环境
deploy:
    docker-compose up -d --build

# 查看日志
logs:
    docker-compose logs -f app

# 清理
clean:
    docker-compose down -v
    docker image prune -f
    rm -rf bin/

9.3 Complete Production-Grade Dockerfile

# ============ 第一阶段:编译 ============
FROM golang:1.22-alpine AS builder

# 安装必要的构建工具
RUN apk add --no-cache git ca-certificates tzdata

# 设置 Go 环境
ENV GOPROXY=https://goproxy.cn,direct
ENV CGO_ENABLED=0
ENV GOOS=linux
ENV GOARCH=amd64

WORKDIR /build

# 依赖缓存层
COPY go.mod go.sum ./
RUN go mod download && go mod verify

# 编译
COPY . .
ARG VERSION=dev
RUN go build \
    -ldflags="-s -w -X main.Version=${VERSION}" \
    -o /build/app \
    ./cmd/server

# ============ 第二阶段:运行 ============
FROM alpine:3.19

LABEL maintainer="your-email@example.com"
LABEL version="${VERSION}"
LABEL description="My Go Application"

# 安装运行时依赖
RUN apk --no-cache add ca-certificates tzdata && \
    cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone

# 创建非 root 用户和必要目录
RUN addgroup -S app && adduser -S app -G app && \
    mkdir -p /app/logs /app/config && \
    chown -R app:app /app

WORKDIR /app

# 复制二进制文件
COPY --from=builder /build/app .

# 复制配置文件模板(可选)
# COPY --from=builder /build/config ./config

# 切换到非 root 用户
USER app

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1

# 启动
ENTRYPOINT ["./app"]

9.4 Complete Workflow Summary

# 1. 编写代码和 Dockerfile
# 2. 本地开发和测试
make dev              # 启动开发环境
make test             # 运行测试

# 3. 构建镜像
make docker-build     # 构建生产镜像

# 4. 本地验证
make deploy           # 启动完整环境
make logs             # 查看日志
curl http://localhost:8080/health  # 测试接口

# 5. 推送镜像
make docker-push      # 推送到仓库

# 6. 部署到服务器
ssh your-server
docker pull registry.example.com/myns/myapp:v1.0
docker-compose up -d

# 7. 清理
make clean

X. Common Issues and Troubleshooting

10.1 Common Issues

Issue Cause Solution
Image size too large Multi-stage build not used Use multi-stage build
Slow build Cache miss COPY go.mod first, then COPY .
Incorrect timezone in container Missing timezone data Install tzdata, set TZ
HTTPS request failed Missing CA certificates apk add ca-certificates
Cannot connect to other containers Network connectivity issue Ensure they are in the same docker network
Data loss Volume not used Use named volumes for important data

10.2 Debugging Tips

# 查看容器内部文件系统
docker exec -it myapp sh

# 查看容器网络配置
docker inspect --format='{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' myapp

# 查看容器环境变量
docker exec myapp env

# 临时运行一个调试容器(和目标容器共享网络)
docker run -it --rm --network container:myapp alpine sh

# 查看 Docker 构建过程(详细输出)
docker build --progress=plain -t myapp . 2>&1 | tee build.log

Next Article: 016 - Introduction to Kubernetes, learn how to deploy Docker containers to a K8s cluster for production-grade orchestration and management.

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6762

(0)
Walker的头像Walker
上一篇 2 hours ago
下一篇 4 hours ago

Related Posts

EN
简体中文 繁體中文 English