# boat-go **Repository Path**: aly1a/boat-go ## Basic Information - **Project Name**: boat-go - **Description**: 一个简单的能快速使用的go语言框架,支持启动服务和连接一些常用的中间件 - **Primary Language**: Go - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-04-15 - **Last Updated**: 2026-09-04 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # boat-go #### 介绍 **Boat-Go 是一个 Go 语言 Web 框架**,可以通过 Go Module 引入到任意项目中使用。它提供开箱即用的 HTTP 服务、路由鉴权和常用中间件(MySQL、Redis、MongoDB、Kafka、ES 等)的统一接入能力,帮助开发者快速搭建服务。 **两种使用方式**: 1. **作为完整框架使用**:引入 `boat` 入口包,自动完成配置加载、日志、HTTP 服务和中间件的初始化与关闭,适合新项目快速起步; 2. **仅作为工具库引入**:只引用需要的模块(如 `module/mysql`、`module/logger`、`util` 等),各模块均可独立初始化、独立关闭,可以逐步接入已有项目。 #### 核心特性 - **高性能 HTTP 服务器**: 基于 fasthttp,支持高并发、低延迟 - **模块化架构**: 清晰的模块划分,支持按需引入 - **丰富的中间件支持**: MySQL(多数据源)、Redis、MongoDB、Kafka(延迟发送)、Elasticsearch 等 - **完善的日志系统**: 基于 zap,支持文件分割、异步写入 - **可观测性**: OpenTelemetry 链路追踪、Prometheus 指标 - **环境变量配置**: 支持 .env.local → .env → env 的配置加载顺序 #### 文档 | 文档 | 说明 | |------|------| | [AGENTS.md](AGENTS.md) | 项目上下文文档 | | [DESIGN.md](DESIGN.md) | 框架设计文档 | | [MODULES.md](MODULES.md) | 模块说明文档 | | [TO_DO.md](TO_DO.md) | 待办事项 | | [doc/mysql_design.md](doc/mysql_design.md) | MySQL 模块设计文档 | | [doc/mysql_usage.md](doc/mysql_usage.md) | MySQL 模块使用文档 | | [doc/kafka_design.md](doc/kafka_design.md) | Kafka 模块设计文档 | | [doc/kafka_usage.md](doc/kafka_usage.md) | Kafka 模块使用文档 | | [doc/mongodb_design.md](doc/mongodb_design.md) | MongoDB 模块设计文档 | | [doc/mongodb_usage.md](doc/mongodb_usage.md) | MongoDB 模块使用文档 | | [doc/redis_design.md](doc/redis_design.md) | Redis 模块设计文档 | | [doc/redis_usage.md](doc/redis_usage.md) | Redis 模块使用文档 | | [doc/postgresql_design.md](doc/postgresql_design.md) | PostgreSQL 模块设计文档 | | [doc/postgresql_usage.md](doc/postgresql_usage.md) | PostgreSQL 模块使用文档 | | [doc/logger_design.md](doc/logger_design.md) | Logger 模块设计文档 | | [doc/logger_usage.md](doc/logger_usage.md) | Logger 模块使用文档 | | [doc/opentelemetry_design.md](doc/opentelemetry_design.md) | OpenTelemetry 模块设计文档 | | [doc/opentelemetry_usage.md](doc/opentelemetry_usage.md) | OpenTelemetry 模块使用文档 | | [doc/sftp_design.md](doc/sftp_design.md) | SFTP 模块设计文档 | | [doc/sftp_usage.md](doc/sftp_usage.md) | SFTP 模块使用文档 | | [doc/es_design.md](doc/es_design.md) | Elasticsearch 模块设计文档 | | [doc/es_usage.md](doc/es_usage.md) | Elasticsearch 模块使用文档 | | [doc/bigCache_design.md](doc/bigCache_design.md) | BigCache 模块设计文档 | | [doc/bigCache_usage.md](doc/bigCache_usage.md) | BigCache 模块使用文档 | | [doc/badgerDB_design.md](doc/badgerDB_design.md) | BadgerDB 模块设计文档 | | [doc/badgerDB_usage.md](doc/badgerDB_usage.md) | BadgerDB 模块使用文档 | | [doc/cronTask_design.md](doc/cronTask_design.md) | CronTask 模块设计文档 | | [doc/cronTask_usage.md](doc/cronTask_usage.md) | CronTask 模块使用文档 | | [doc/fastHttp_design.md](doc/fastHttp_design.md) | FastHttp 模块设计文档 | | [doc/fastHttp_usage.md](doc/fastHttp_usage.md) | FastHttp 模块使用文档 | | [doc/httptransport_design.md](doc/httptransport_design.md) | HttpTransport 模块设计文档 | | [doc/httptransport_usage.md](doc/httptransport_usage.md) | HttpTransport 模块使用文档 | | [doc/excel_design.md](doc/excel_design.md) | Excel 模块设计文档 | | [doc/excel_usage.md](doc/excel_usage.md) | Excel 模块使用文档 | #### 软件架构 软件架构说明 #### 安装教程 要求 Go 版本 >= 1.24.4。 ```bash go get gitee.com/aly1a/boat-go ``` 引入后按需导入子包: ```go import ( "gitee.com/aly1a/boat-go/boat" // 整框架入口 "gitee.com/aly1a/boat-go/module/mysql" // 或仅引入单个模块 "gitee.com/aly1a/boat-go/module/logger" "gitee.com/aly1a/boat-go/util" ) ``` 1、在工作目录中生成配置文件,配置文件加载顺序是 .env.local --> .env --> env(可参考仓库中的 `env` 模板文件,复制为 `.env.local` 后修改) ``` app service # app service 端口 默认3000 APP_PORT=3000 # app 指标服务 端口 默认3001 APP_METRICS_PORT=3001 # http ReadTimeout是读取整个请求(包括请求体)的最大持续时间。零或负值表示没有超时。因为ReadTimeout不允许处理程序对每个请求体的可接受截止日期或上传速率做出每个请求的决定,所以大多数用户更喜欢使用ReadHeaderTimeout。两者都使用是有效的。 # fasthttp ReadTimeout是允许读取完整请求(包括body)的时间。在连接打开时重置连接的读取截止日期,或者在读取第一个字节后重置保持连接的读取截止日期。 # fasthttp 默认情况下,请求读取超时时间是不限制的. # 单位ms APP_APPREADTIMEOUT= # http IdleTimeout是启用keep-alives时等待下一个请求的最大时间。如果IdleTimeout为零,则使用ReadTimeout的值。如果两者都是ero,就没有超时。 # fasthttp IdleTimeout是启用keep-alive时等待下一个请求的最大时间。如果IdleTimeout为零,则使用ReadTimeout的值。 # 单位 ms APP_APPIDLETIMEOUT= # http WriteTimeout是响应写入前的最大持续时间。每当读取新请求的标题时,它都会重置。与ReadTimeout一样,它不允许处理人员根据每个请求做出决定。 零或负值意味着不会超时。 # fasthttp WriteTimeout是响应写超时的最大持续时间。它在请求处理程序返回后被重置。 # fastHttp 启动的服务的 WriteTimeout 和http不一样,http需要总时长,fastHttp只是单步骤时长 # 单位 ms APP_APPWRITETIMEOUT= # 是否开启tcp保持连接。 操作系统是否在tcp连接上发送tcp keep-alive消息。 默认开启 APP_KEEPALIVE= # fasthttp tcp保持连接消息之间的间隔时间。缺省情况下,TCP的存活时间由操作系统决定。 # 单位ms APP_KEEPALIVEPERIOD= # fasthttp Concurrency 服务器的最大并发连接数。 APP_CONCURRENCY= # fasthttp 每个IP允许的最大并发客户端连接数。默认情况下,可以从单个IP地址建立无限数量的并发连接。 APP_MAXCONNSPERIP= # fasthttp 最大请求体大小。 # fasthttp 服务器拒绝正文超过此限制的请求。 # fasthttp 默认情况下,请求体大小由DefaultMaxRequestBodySize限制。 # fasthttp 默认为 4 * 1024 * 1024 APP_MAXREQUESTBODYSIZE= # 个连接的请求读取缓冲区大小。这也限制了最大头部大小。如果您的客户端发送多KB的RequestURIs和/或多KB的头部(例如,大cookies),请增加此缓冲区。未设置时使用默认缓冲区大小。 APP_READBUFFERSIZE= ``` ``` opentelemetry # 是否开启 opentelemetry 指标监控 默认关闭 false 关闭; true 开启 # 需要在手动引入 "gitee.com/aly1a/boat-go/module/opentelemetry" 模块,否则无效 APP_OPENTELEMETRY_ENABLE= # opentelemetry collector 地址类型 默认 RPC 支持 HTTP、RPC 两种类型 APP_OPENTELEMETRY_COLLECTOR_TYPE= # opentelemetry collector 地址 APP_OPENTELEMETRY_COLLECTOR_URL=localhost:4317 # 设置采样率 默认 0.01 APP_OPENTELEMETRY_TP_SAMPLER_RATIOBASED= # 连接 otel-collector 是否重试 默认 true APP_OPENTELEMETRY_COLLECTOR_RETRY_ENABLED= # 连接 otel-collector 第一次重试的间隔时间 单位秒 默认 3 APP_OPENTELEMETRY_COLLECTOR_RETRY_INITIALINTERVAL= # 连接 otel-collector 以后重试间隔时间最大值 默认 5 APP_OPENTELEMETRY_COLLECTOR_RETRY_MAXINTERVAL= # 连接 otel-collector 最大重试时间(此时间后抛弃数据)默认18 APP_OPENTELEMETRY_COLLECTOR_RETRY_MAXELAPSEDTIME= ``` ``` log # 日志级别 日志打印级别 debug info warning error 默认debug APP_LOG_LEVEL= # 输出日志格式 logfmt, json 默认 logfmt APP_LOG_FORMAT= # 输出日志文件路径 默认 ./logs/ APP_LOG_PATH= # 输出日志文件名称 默认log APP_LOG_FILENAME= # 【日志分割】单个日志文件最多存储量 单位(mb) 默认 500 APP_LOG_FILEMAXSIZE= # 【日志分割】日志备份文件最多数量 默认 2 APP_LOG_FILEMAXBACKUPS= # 日志保留时间,单位: 天 (day) 默认 7 APP_LOG_FILEMAXAGE= # 是否压缩日志 默认false APP_LOG_COMPRESS= # 是否输出到控制台 默认 true APP_LOG_STDOUT= # 是否异步输出到控制台 APP_LOG_STDOUTSYNC= # 项目启动时启用新的日志文件 默认true APP_LOG_STARTNEWFILE= # 是否输出日志到文件 默认true APP_LOG_FILE= # 日志如库工作者数量 默认1 APP_LOG_INDBWORKER_NUM= ``` ``` mongodb # mongodb配置 APP_MONGODB_CONNECT_URL=mongodb://@host.docker.internal:27027/?authSource=admin&minPoolSize=10&maxPoolSize=100 # SetConnectTimeout指定用于创建与服务器连接的超时时间ms APP_MONGODB_CONNECT_TIMEOUT=500 # SetSocketTimeout指定驱动程序在返回之前等待套接字读写返回多长时间 APP_MONGODB_SOCKET_TIMEOUT=500 # SetTimeout指定在此客户端上运行的单个操作在返回错误之前可以执行的时间 APP_MONGODB_TIMEOUT=500 ``` ``` fastHttp # fastHttp 连接池配置 # 每台主机可以建立的最大连接数。 默认 512 PROXY_MAXIDLECONNSPERHOST= # 空闲的keep-alive连接在此时间后关闭。默认情况下,在DefaultMaxIdleConnDuration之后关闭空闲连接。 # 默认 5000 单位ms。 # 由于下游模块有1秒以上的延迟,可以适当延长空闲连接的存活时间,以便复用连接,减少建立新连接的开销。例如,设置为 10 * time.Second 或更大,根据平均请求间隔和资源情况调整。 PROXY_IDLECONNTIMEOUT= # Keep-alive连接在此持续时间后关闭。 # 默认情况下,连接时间是不限制的。单位ms PROXY_CONNTIMEOUT= # 幂等调用的最大尝试次数。 # 默认1次 PROXY_MAXCALLATTEMPTS= # 完整响应读取(包括body)的最大持续时间。 # 限制等待服务器响应的最长时间。太短可能导致正常请求被误判为超时,太长则可能影响请求的响应速度。需要根据业务需求和网络状况设定,默认 5000 单位ms。 PROXY_REP_HEADER_TIMEOUT= # 完整写入请求(包括请求体)的最大持续时间。 # 同样用于限制请求发送的超时时间,设置原则同上。默认 500 单位ms PROXY_WRITETIMEOUT= ``` ``` http # http 连接池配置 # 出代理 AXIOS_HTTP_PROXY= # keep alive 探活时间 APP_HTTPPOOL_KEEPALIVE_DETECTTIME= # 连接建立超时时间 APP_HTTPPOOL_DIALTIMEOUT= # 响应头读取超时时间 它用于设置从发送HTTP请求开始到收到服务器响应头的超时时间 APP_HTTPPOOL_RESPONSE_TIMEOUT= # 设置整个Transport的最大空闲连接数。增大此值可以减少新建连接的开销,但过高会占用更多资源。一般推荐设置为预期并发请求数的几倍 APP_HTTPPOOL_MAXIDLECONNS= # 设置空闲连接的超时时间,超时后连接将被关闭并从连接池中移除 APP_HTTPPOOL_IDLECONNTIMEOUT= # DisableKeepAlives,如果为真,将禁用HTTP保活,并且仅使用连接到服务器的连接进行单个HTTP请求。这与同名TCP保活无关。 APP_HTTPPOOL_DISABLEKEEPALIVES= # 每个主机的最大空闲连接数。在高并发请求同一主机时,增加此值可以减少建立新连接的频率,提高效率。根据目标服务器的能力和并发需求进行调整 APP_HTTPPOOL_MAXIDLECONNSPERHOST= # InsecureSkipVerify控制客户端是否验证服务器的证书链和主机名。如果InsecureSkipVerify为true,crypto/tls接受服务器提供的任何证书以及该证书中的任何主机名。在此模式下,TLS容易受到中间人攻击,除非使用自定义验证。这仅应用于测试或与VerifyConnection或VerifyPeerCertificate结合使用。 默认 false # APP_HTTPPOOL_TLSCLIENTC_INSECURESKIPVERIFY= # ForceAttemptHTTP2控制当提供了非零Dial, DialTLS或DialContext函数或TLSClientConfig时是否启用HTTP/2。默认情况下,使用这些字段保守地禁用HTTP/2。要使用自定义拨号器或TLS配置并仍然尝试HTTP/2升级,请将此设置为true。默认 true # APP_HTTPPOOL_HTTP2_FORCEATTEMPT= ``` ``` kafka # kafka reader 配置 # 用于连接到kafka集群的代理地址列表。 localhost:9092,localhost:9093,localhost:9094 # APP_KAFKA_READER_BROKERS= # topic # APP_KAFKA_READER_TOPIC= # GroupID包含可选的用户组id。如果指定了GroupID,则不应该指定分区,例如0 # test # APP_KAFKA_READER_GROUP_ID= # GroupTopics允许指定多个主题,但只能与GroupID结合使用,因为它是一个消费者组特性。因此,如果设置了GroupID,则必须定义Topic或GroupTopics中的一个。 # test,test2 # APP_KAFKA_READER_GROUP_TOPICS= # 分区 不能和 group 一起使用 # APP_KAFKA_READER_PARTITION= # StartOffset确定消费者组在找到没有提交偏移量的分区时应从何处开始消费。如果非零,则必须将其设置为FirstOffset或LastOffset之一。默认:FirstOffset。仅在设置GroupID时使用. LastOffset\FirstOffset # APP_KAFKA_READER_STARTOFFSET= # 内部消息队列的容量,如果未设置则默认为100。 # APP_KAFKA_READER_QUEUECAPACITY= # MinBytes向代理指示使用者将接受的最小批处理大小。当从小容量主题消费时设置较高的最小值,当代理没有足够的数据来满足定义的最小值时,可能会导致延迟交付。 # APP_KAFKA_READER_MINBYTES= # MaxBytes向代理指示使用者将接受的最大批处理大小。当从大容量主题消费时设置较高的最大值,以允许代理在网络延迟或消费者负载时提供更大的批处理。 # APP_KAFKA_READER_MAXBYTES= # 当从kafka获取批量消息时,等待新数据的最大时间。 # APP_KAFKA_READER_MAXWAIT= # ReadBatchTimeout从kafka消息批处理中获取消息的等待时间。 # APP_KAFKA_READER_READBATCHTIMEOUT= # ReadLagInterval设置读取器延迟更新的频率。 # 将该字段设置为负值将禁用延迟报告。 # APP_KAFKA_READER_READLAGINTERVAL= # HeartbeatInterval设置读取器发送消费者组心跳更新的可选频率。 # APP_KAFKA_READER_HEARTBEATINTERVAL= # 在返回错误之前尝试连接的次数限制。 默认3次 # APP_KAFKA_READER_MAXATTEMPTS= # CommitInterval表示将偏移量提交到代理的间隔。如果为0,提交将同步处理。默认:0 仅当设置了GroupID时使用 # APP_KAFKA_READER_COMMITINTERVAL= # PartitionWatchInterval表示读取器检查分区变化的频率。如果读取器看到分区变化(如添加分区),它将重新平衡组以获取新的分区。默认:5秒。仅在设置GroupID和WatchPartitionChanges时使用。 # APP_KAFKA_READER_PARTITIONWATCHINTERVAL= # 会话超时可选地设置在没有心跳的情况下可能过去的时间长度,在此期间协调器认为消费者已死亡并启动重新平衡。默认:30秒 仅当设置了GroupID时使用 # APP_KAFKA_READER_SESSIONTIMEOUT= # 重新平衡超时可选地设置了协调器等待成员加入重新平衡过程的时间长度。对于负载较高的kafka服务器,将此值设置得更高可能是有用的。默认:30秒 仅在设置了GroupID时使用 # APP_KAFKA_READER_REBALANCETIMEOUT= # JoinGroupBackoff 可选地设置在错误后重新加入消费者组之间的等待时间长度。默认:5秒 # APP_KAFKA_READER_JOINCROUPBACKOFF= # BackoffDelayMin 可选地设置读取器在轮询新消息之前等待的最短时间。默认:100ms # APP_KAFKA_READER_READBACKOFFMIN= # BackoffDelayMax 可选地设置读取器在轮询新消息之前将等待的最大时间。默认:1秒 # APP_KAFKA_READER_READBACKOFFMAX= # 连接尝试次数的限制,在返回错误之前。默认尝试3次。 # APP_KAFKA_READER_MAXATTEMPTS= # WatchForPartitionChanges用于通知kafka-go,如果主题发生任何分区变化,消费者组应该轮询代理并进行重新平衡。 # APP_KAFKA_READER_EATCHPARTITIONCHANGES= # OffsetOutOfRangeError 表示在发生偏移量超出范围错误时,读取器应返回错误,而不是无限期地重试。此标志被添加以保持向后兼容性,因此它将在 kafka-go 的未来版本中被移除。 # APP_KAFKA_READER_OFFSETOUTOFRANGEERROR= # kafka writer # 用于连接到kafka集群的代理地址列表。 localhost:9092,localhost:9093,localhost:9094 # APP_KAFKA_WRITER_BROKERS= # topic # APP_KAFKA_WRITER_TOPIC= # 消息投递尝试次数的限制。默认最多尝试10次。 # APP_KAFKA_WRITER_MAXATTEMPTS= # 发送到分区前的消息缓冲限制。默认使用目标批次大小为100条消息。 # APP_KAFKA_WRITER_BATCHSIZE= # 限制发送到分区之前请求的最大字节数。默认使用 Kafka 默认值 1048576。 # APP_KAFKA_WRITER_BATCHBYTES= # 消息批次不完整时刷新到Kafka的时间限制。默认至少每秒刷新一次。 # "300ms", "-1.5h" or "2h45m" # APP_KAFKA_WRITER_BATCHTIMEOUT= # 写入器执行的读取操作的超时时间。默认为10秒。 # APP_KAFKA_WRITER_READTIMEOUT= # 写入操作超时。默认为10秒。 # APP_KAFKA_WRITER_WRITETIMEOUT= # 在接收到生产请求的响应之前,需要从分区副本中确认的数量。默认值为-1,表示等待所有副本,0以上的值表示需要多少个副本确认消息才能被视为成功。 # APP_KAFKA_WRITER_REQUIREDACKS= # 将此标志设置为true会导致WriteMessages方法永远不会阻塞。这也意味着错误会被忽略,因为调用者将不会收到返回值。只有在你不在乎消息是否被写入kafka的保证时才使用此功能。 # APP_KAFKA_WRITER_ASYNC= # 是否启用压缩 默认 false # APP_KAFKA_WRITER_COMPRESSION= # 连接 dialer 设置 拨号超时时间 默认 不超时 如:1s # APP_KAFKA_WRITER_DIALER_TIMEOUT= # 连接 dialer 设置 keepAlive 探活时间 默认 不探活 如:30s # APP_KAFKA_WRITER_DIALER_KEEPALIVE= ``` ``` mysql # mysql 配置 # mysql 连接地址 uri APP_MYSQL_CONNECT_URL= # 设置到数据库的最大打开连接数。如果MaxIdleConns大于0并且新的MaxOpenConns小于MaxIdleConns,则MaxIdleConns将被减少以匹配新的MaxOpenConns限制。如果n<=0,则打开连接数没有限制。默认值为0(无限制)。 # APP_MYSQL_MAXOPENCONNS= # 设置空闲连接池中的最大连接数。如果n<=0,则不保留空闲连接。默认的最大空闲连接当前为2。这可能会在未来的版本中更改。 # APP_MYSQL_MAXIDLECONNS= # 设置连接可以重用的最长时间。过期的连接可能会在重用前延迟关闭。如果d<=0,则由于连接的年龄而不会关闭连接。 单位s # APP_MYSQL_CONNMAXLIFETIMEINT= # 多数据源配置(JSON 格式) # APP_MYSQL_CONNECTS=[{"tag":"master","addr":"user:pass@tcp(master:3306)/db"},{"tag":"slave","addr":"user:pass@tcp(slave:3306)/db"}] ``` ``` sftp # SFTP 配置 # sftp host APP_SFTP_HOST= # sftp port APP_SFTP_PORT= # sftp username APP_SFTP_USERNAME= # sftp password APP_SFTP_PASSWORD= # 超时时间 单位ms APP_SFTP_TIMEOUT=1000 # 根目录 APP_SFTP_REMOTEDIR=/test # (已废弃的 util.ConnectSftp)known_hosts 文件路径,设置后启用主机密钥严格校验; # 未设置时保持兼容行为但输出安全警告。新代码请使用 module/sftp 包并配置 HostKey # SFTP_KNOWN_HOSTS=/path/to/known_hosts ``` #### 使用说明 **方式一:作为完整框架使用**(推荐新项目) 1. 在业务项目的 `init()` 中调用 `boat.Init()`,框架会自动完成环境变量加载、日志初始化、应用配置加载; 2. 通过 `app/routers` 注册路由和鉴权函数; 3. 在 `main()` 中调用 `boat.Start()` 启动服务(阻塞等待关闭信号,收到 SIGINT/SIGTERM 后自动优雅停机); 4. 需要额外中间件时(MySQL/Redis/Kafka 等),在 `init()` 中按环境变量开关调用对应模块的初始化函数。 **方式二:仅作为模块库引入**(适合已有项目逐步接入) 各模块可独立使用,互不依赖启动流程,例如只引入 MySQL 模块: ```go import ( "gitee.com/aly1a/boat-go/module/mysql" ) func init() { // 方式一:传 nil 从环境变量加载配置(APP_MYSQL_* 系列) if err := mysql.Init(nil); err != nil { panic(err) } // 方式二:代码传入配置 // if err := mysql.Connect(mysql.MysqlConfig_T{Tag: "master", Addr: "user:pass@tcp(127.0.0.1:3306)/db"}); err != nil { // panic(err) // } } func main() { db, _ := mysql.ClientDefault() _ = db.QueryRow("SELECT 1") defer mysql.Close() } ``` 所有模块遵循统一的 `Init/Connect` + `Close` 约定,可组合引入任意子集。各模块的详细用法见 `doc/` 目录对应文档。 #### 快速开始 ```go package main import ( "gitee.com/aly1a/boat-go/boat" "gitee.com/aly1a/boat-go/app/routers" ) func init() { boat.Init() // 注册路由 routers.AddFunc("/api", "hello", 0, nil, func(b *routers.Base_T) { b.ResultSuccess("Hello World") }) } func main() { boat.Start() defer boat.Close() } ``` 更多示例请参考 `demo/` 目录。