+
+
-接口功能不完善,仅作为示例
+
-* 用户登录数据保存在内存中,单次运行过程中有效
-* 视频上传后会保存到本地 public 目录中,访问时用 127.0.0.1:8080/static/video_name 即可
+### 后端服务器
-### 测试
+
-test 目录下为不同场景的功能测试case,可用于验证功能实现正确性
+## 开发组成员
-其中 common.go 中的 _serverAddr_ 为服务部署的地址,默认为本机地址,可以根据实际情况修改
+| 成员 | 联系方式 |
+| --------- | ------------------- |
+| 云隐 | yunyin_jayyi@qq.com |
+| Echo | 2116018091@qq.com |
+| 小封 | 3577536707@qq.com |
+| 醉梦 | 1184387860@qq.com |
+| 老八 | 2986566788@qq.com |
+| ABYSMILER | 2932418551@qq.com |
+| 布伦达 | Ely17520@163.com |
-测试数据写在 demo_data.go 中,用于列表接口的 mock 测试
\ No newline at end of file
diff --git a/assets/image-20230907092728533.png b/assets/image-20230907092728533.png
new file mode 100644
index 00000000..2a863038
Binary files /dev/null and b/assets/image-20230907092728533.png differ
diff --git a/assets/image-20230907092903070.png b/assets/image-20230907092903070.png
new file mode 100644
index 00000000..ef5a959f
Binary files /dev/null and b/assets/image-20230907092903070.png differ
diff --git a/assets/image-20230907092945095.png b/assets/image-20230907092945095.png
new file mode 100644
index 00000000..3001ec81
Binary files /dev/null and b/assets/image-20230907092945095.png differ
diff --git a/assets/image-20230907093014712.png b/assets/image-20230907093014712.png
new file mode 100644
index 00000000..f8b6edaf
Binary files /dev/null and b/assets/image-20230907093014712.png differ
diff --git a/controller/comment.go b/controller/comment.go
deleted file mode 100644
index 6875a1e8..00000000
--- a/controller/comment.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package controller
-
-import (
- "github.com/gin-gonic/gin"
- "net/http"
-)
-
-type CommentListResponse struct {
- Response
- CommentList []Comment `json:"comment_list,omitempty"`
-}
-
-type CommentActionResponse struct {
- Response
- Comment Comment `json:"comment,omitempty"`
-}
-
-// CommentAction no practical effect, just check if token is valid
-func CommentAction(c *gin.Context) {
- token := c.Query("token")
- actionType := c.Query("action_type")
-
- if user, exist := usersLoginInfo[token]; exist {
- if actionType == "1" {
- text := c.Query("comment_text")
- c.JSON(http.StatusOK, CommentActionResponse{Response: Response{StatusCode: 0},
- Comment: Comment{
- Id: 1,
- User: user,
- Content: text,
- CreateDate: "05-01",
- }})
- return
- }
- c.JSON(http.StatusOK, Response{StatusCode: 0})
- } else {
- c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
- }
-}
-
-// CommentList all videos have same demo comment list
-func CommentList(c *gin.Context) {
- c.JSON(http.StatusOK, CommentListResponse{
- Response: Response{StatusCode: 0},
- CommentList: DemoComments,
- })
-}
diff --git a/controller/demo_data.go b/controller/demo_data.go
deleted file mode 100644
index cc10ff36..00000000
--- a/controller/demo_data.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package controller
-
-var DemoVideos = []Video{
- {
- Id: 1,
- Author: DemoUser,
- PlayUrl: "https://www.w3schools.com/html/movie.mp4",
- CoverUrl: "https://cdn.pixabay.com/photo/2016/03/27/18/10/bear-1283347_1280.jpg",
- FavoriteCount: 0,
- CommentCount: 0,
- IsFavorite: false,
- },
-}
-
-var DemoComments = []Comment{
- {
- Id: 1,
- User: DemoUser,
- Content: "Test Comment",
- CreateDate: "05-01",
- },
-}
-
-var DemoUser = User{
- Id: 1,
- Name: "TestUser",
- FollowCount: 0,
- FollowerCount: 0,
- IsFollow: false,
-}
diff --git a/controller/favorite.go b/controller/favorite.go
deleted file mode 100644
index 5f19213f..00000000
--- a/controller/favorite.go
+++ /dev/null
@@ -1,27 +0,0 @@
-package controller
-
-import (
- "github.com/gin-gonic/gin"
- "net/http"
-)
-
-// FavoriteAction no practical effect, just check if token is valid
-func FavoriteAction(c *gin.Context) {
- token := c.Query("token")
-
- if _, exist := usersLoginInfo[token]; exist {
- c.JSON(http.StatusOK, Response{StatusCode: 0})
- } else {
- c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
- }
-}
-
-// FavoriteList all users have same favorite video list
-func FavoriteList(c *gin.Context) {
- c.JSON(http.StatusOK, VideoListResponse{
- Response: Response{
- StatusCode: 0,
- },
- VideoList: DemoVideos,
- })
-}
diff --git a/controller/feed.go b/controller/feed.go
deleted file mode 100644
index b4ac439a..00000000
--- a/controller/feed.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package controller
-
-import (
- "github.com/gin-gonic/gin"
- "net/http"
- "time"
-)
-
-type FeedResponse struct {
- Response
- VideoList []Video `json:"video_list,omitempty"`
- NextTime int64 `json:"next_time,omitempty"`
-}
-
-// Feed same demo video list for every request
-func Feed(c *gin.Context) {
- c.JSON(http.StatusOK, FeedResponse{
- Response: Response{StatusCode: 0},
- VideoList: DemoVideos,
- NextTime: time.Now().Unix(),
- })
-}
diff --git a/controller/publish.go b/controller/publish.go
deleted file mode 100644
index 6990f85a..00000000
--- a/controller/publish.go
+++ /dev/null
@@ -1,59 +0,0 @@
-package controller
-
-import (
- "fmt"
- "github.com/gin-gonic/gin"
- "net/http"
- "path/filepath"
-)
-
-type VideoListResponse struct {
- Response
- VideoList []Video `json:"video_list"`
-}
-
-// Publish check token then save upload file to public directory
-func Publish(c *gin.Context) {
- token := c.PostForm("token")
-
- if _, exist := usersLoginInfo[token]; !exist {
- c.JSON(http.StatusOK, Response{StatusCode: 1, StatusMsg: "User doesn't exist"})
- return
- }
-
- data, err := c.FormFile("data")
- if err != nil {
- c.JSON(http.StatusOK, Response{
- StatusCode: 1,
- StatusMsg: err.Error(),
- })
- return
- }
-
- filename := filepath.Base(data.Filename)
- user := usersLoginInfo[token]
- finalName := fmt.Sprintf("%d_%s", user.Id, filename)
- saveFile := filepath.Join("./public/", finalName)
- if err := c.SaveUploadedFile(data, saveFile); err != nil {
- c.JSON(http.StatusOK, Response{
- StatusCode: 1,
- StatusMsg: err.Error(),
- })
- return
- }
-
- c.JSON(http.StatusOK, Response{
- StatusCode: 0,
- StatusMsg: finalName + " uploaded successfully",
- })
-}
-
-// PublishList all users have same publish video list
-func PublishList(c *gin.Context) {
- c.JSON(http.StatusOK, VideoListResponse{
- Response: Response{
- StatusCode: 0,
- },
- VideoList: DemoVideos,
- })
-}
diff --git a/controller/user.go b/controller/user.go
deleted file mode 100644
index 72fc57ec..00000000
--- a/controller/user.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package controller
-
-import (
- "github.com/gin-gonic/gin"
- "net/http"
- "sync/atomic"
-)
-
-// usersLoginInfo use map to store user info, and key is username+password for demo
-// user data will be cleared every time the server starts
-// test data: username=zhanglei, password=douyin
-var usersLoginInfo = map[string]User{
- "zhangleidouyin": {
- Id: 1,
- Name: "zhanglei",
- FollowCount: 10,
- FollowerCount: 5,
- IsFollow: true,
- },
-}
-
-var userIdSequence = int64(1)
-
-type UserLoginResponse struct {
- Response
- UserId int64 `json:"user_id,omitempty"`
- Token string `json:"token"`
-}
-
-type UserResponse struct {
- Response
- User User `json:"user"`
-}
-
-func Register(c *gin.Context) {
- username := c.Query("username")
- password := c.Query("password")
-
- token := username + password
-
- if _, exist := usersLoginInfo[token]; exist {
- c.JSON(http.StatusOK, UserLoginResponse{
- Response: Response{StatusCode: 1, StatusMsg: "User already exist"},
- })
- } else {
- atomic.AddInt64(&userIdSequence, 1)
- newUser := User{
- Id: userIdSequence,
- Name: username,
- }
- usersLoginInfo[token] = newUser
- c.JSON(http.StatusOK, UserLoginResponse{
- Response: Response{StatusCode: 0},
- UserId: userIdSequence,
- Token: username + password,
- })
- }
-}
-
-func Login(c *gin.Context) {
- username := c.Query("username")
- password := c.Query("password")
-
- token := username + password
-
- if user, exist := usersLoginInfo[token]; exist {
- c.JSON(http.StatusOK, UserLoginResponse{
- Response: Response{StatusCode: 0},
- UserId: user.Id,
- Token: token,
- })
- } else {
- c.JSON(http.StatusOK, UserLoginResponse{
- Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"},
- })
- }
-}
-
-func UserInfo(c *gin.Context) {
- token := c.Query("token")
-
- if user, exist := usersLoginInfo[token]; exist {
- c.JSON(http.StatusOK, UserResponse{
- Response: Response{StatusCode: 0},
- User: user,
- })
- } else {
- c.JSON(http.StatusOK, UserResponse{
- Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"},
- })
- }
-}
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/README.md" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/README.md"
new file mode 100644
index 00000000..d34d7802
--- /dev/null
+++ "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/README.md"
@@ -0,0 +1,5 @@
+# 项目需求
+
+* 业务参考系统:用于存放搭建业务项目相关文档,包括:数据库脚本,搭建教程,需求文档等等。
+* 业务流程文档:用于存放梳理的业务流程图以及补充原型设计等等。
+
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/README.md" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/README.md"
new file mode 100644
index 00000000..4a6831fc
--- /dev/null
+++ "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/README.md"
@@ -0,0 +1,27 @@
+# 项目搭建教程
+
+## 1 准备代码和数据库
+
+通过下面方式克隆代码:
+
+```
+git clone https://github.com/life-studied/douyin-simple.git
+```
+
+执行`sqls`目录下面的`douyin_simple.sql`脚本,构建数据库和表,注意仔细阅读该目录下面的`README.md`。
+
+## 2 搭建后端
+
+将项目导入Goland,等待项目初始化完成。
+
+### 2.1 修改配置
+
+修改`config.yaml`配置文件
+
+### 2.2 启动项目
+
+找到`main.go`程序入口,点击运行,开始编译和运行程序。
+
+## 3.搭建前端
+
+https://bytedance.feishu.cn/docx/NMneddpKCoXZJLxHePUcTzGgnmf
\ No newline at end of file
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/README.md" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/README.md"
new file mode 100644
index 00000000..872ea1df
--- /dev/null
+++ "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/README.md"
@@ -0,0 +1,8 @@
+# 执行`SQL`注意事项
+
+***注意:这里存放初始的数据库结构,如果后续根据业务需要对数据库进行了修改,需要将新的数据库脚本备份到此目录下面。***
+
+`SQL`文件说明:
+
+- `reference-project.sql`:用于搭建项目本地测试时使用的数据库,直接导入完成建库建表。
+- `douyin-simple.sql`:是项目开发使用的数据库,直接导入完成建库建表。
\ No newline at end of file
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/douyin-simple.sql" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/douyin-simple.sql"
new file mode 100644
index 00000000..a1a1826c
--- /dev/null
+++ "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/douyin-simple.sql"
@@ -0,0 +1,52 @@
+-- 创建数据库
+CREATE DATABASE IF NOT EXISTS douyin_simple;
+
+-- 使用数据库
+USE douyin_simple;
+
+-- 创建 users 表
+CREATE TABLE IF NOT EXISTS users (
+ id BIGINT PRIMARY KEY COMMENT '用户ID',
+ name VARCHAR(255) NOT NULL COMMENT '用户名',
+ password VARCHAR(255) NOT NULL COMMENT '密码'
+);
+
+-- 创建 videos 表
+CREATE TABLE IF NOT EXISTS videos (
+ id BIGINT PRIMARY KEY COMMENT '视频ID',
+ author_id BIGINT NOT NULL COMMENT '作者ID',
+ play_url VARCHAR(255) NOT NULL COMMENT '播放链接',
+ cover_url VARCHAR(255) NOT NULL COMMENT '封面链接',
+ title VARCHAR(255) NOT NULL COMMENT '标题',
+ publish_time DATETIME NOT NULL COMMENT '发布时间戳',
+ FOREIGN KEY (author_id) REFERENCES users(id)
+);
+
+-- 创建 follows 表
+CREATE TABLE IF NOT EXISTS follows (
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ follow_user_id BIGINT NOT NULL COMMENT '被关注的用户ID',
+ PRIMARY KEY (user_id, follow_user_id),
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (follow_user_id) REFERENCES users(id) ON DELETE CASCADE
+);
+
+-- 创建 comments 表
+CREATE TABLE IF NOT EXISTS comments (
+ id BIGINT PRIMARY KEY COMMENT '评论ID',
+ user_id BIGINT NOT NULL COMMENT '用户ID',
+ video_id BIGINT NOT NULL COMMENT '视频ID',
+ content TEXT NOT NULL COMMENT '评论内容',
+ create_date DATETIME NOT NULL COMMENT '创建日期',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE
+);
+
+-- 创建 likes 表
+CREATE TABLE IF NOT EXISTS likes (
+ id BIGINT NOT NULL COMMENT '主键ID',
+ user_id BIGINT NOT NULL COMMENT '点赞者ID',
+ video_id BIGINT NOT NULL COMMENT '视频ID',
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
+ FOREIGN KEY (video_id) REFERENCES videos(id) ON DELETE CASCADE
+);
\ No newline at end of file
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/reference-project.sql" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/reference-project.sql"
new file mode 100644
index 00000000..9aa2aa01
--- /dev/null
+++ "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\345\217\202\350\200\203\347\263\273\347\273\237/sqls/reference-project.sql"
@@ -0,0 +1,41 @@
+-- 使用数据库
+USE douyin_simple;
+
+-- 插入 users 表数据
+INSERT INTO users (id, name, password) VALUES
+(1, 'user_1', 'password_1'),
+(2, 'user_2', 'password_2'),
+(3, 'user_3', 'password_3'),
+(4, 'user_4', 'password_4'),
+(5, 'user_5', 'password_5');
+
+-- 插入 videos 表数据
+INSERT INTO videos (id, author_id, play_url, cover_url, title, publish_time) VALUES
+(1, 1, 'play_url_1', 'cover_url_1', 'title_1', '2022-01-01 10:00:00'),
+(2, 1, 'play_url_2', 'cover_url_2', 'title_2', '2022-02-01 14:30:00'),
+(3, 2, 'play_url_3', 'cover_url_3', 'title_3', '2022-03-12 09:15:00'),
+(4, 3, 'play_url_4', 'cover_url_4', 'title_4', '2022-04-25 18:45:00'),
+(5, 4, 'play_url_5', 'cover_url_5', 'title_5', '2022-05-09 21:20:00');
+
+-- 插入 follows 表数据
+INSERT INTO follows (user_id, follow_user_id) VALUES
+(1, 2),
+(1, 3),
+(2, 4),
+(3, 1),
+(4, 5);
+
+-- 插入 comments 表数据
+INSERT INTO comments (id, user_id, video_id, content, create_date) VALUES
+(1, 2, 1, 'comment_1', '2022-01-01 12:30:00'),
+(2, 3, 1, 'comment_2', '2022-01-02 09:45:00'),
+(3, 1, 3, 'comment_3', '2022-03-14 13:10:00'),
+(4, 5, 2, 'comment_4', '2022-02-02 16:20:00');
+
+-- 插入 likes 表数据
+INSERT INTO likes (id, user_id, video_id) VALUES
+(1, 1, 2),
+(2, 2, 1),
+(3, 3, 3),
+(4, 4, 5),
+(5, 5, 4);
diff --git a/public/data "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep"
similarity index 100%
rename from public/data
rename to "documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep"
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\210\240\351\231\244\350\257\204\350\256\272.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\210\240\351\231\244\350\257\204\350\256\272.png"
new file mode 100644
index 00000000..5662ab61
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\210\240\351\231\244\350\257\204\350\256\272.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\350\257\204\350\256\272.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\350\257\204\350\256\272.png"
new file mode 100644
index 00000000..6cc6ca48
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\350\257\204\350\256\272.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\216\267\345\217\226\350\257\204\350\256\272.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\216\267\345\217\226\350\257\204\350\256\272.png"
new file mode 100644
index 00000000..c598ef2a
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\216\267\345\217\226\350\257\204\350\256\272.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep"
new file mode 100644
index 00000000..e69de29b
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\345\210\227\350\241\250\346\216\245\345\217\243.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\345\210\227\350\241\250\346\216\245\345\217\243.png"
new file mode 100644
index 00000000..507ab729
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\217\221\345\270\203\345\210\227\350\241\250\346\216\245\345\217\243.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\226\234\346\254\242\345\210\227\350\241\250.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\226\234\346\254\242\345\210\227\350\241\250.png"
new file mode 100644
index 00000000..04c37cf9
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\345\226\234\346\254\242\345\210\227\350\241\250.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\202\271\350\265\236.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\202\271\350\265\236.png"
new file mode 100644
index 00000000..9d43de5e
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\202\271\350\265\236.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\344\277\241\346\201\257\346\216\245\345\217\243.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\344\277\241\346\201\257\346\216\245\345\217\243.png"
new file mode 100644
index 00000000..047ba7c9
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\344\277\241\346\201\257\346\216\245\345\217\243.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\346\263\250\345\206\214\346\216\245\345\217\243.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\346\263\250\345\206\214\346\216\245\345\217\243.png"
new file mode 100644
index 00000000..c310c86b
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\346\263\250\345\206\214\346\216\245\345\217\243.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\347\231\273\345\275\225.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\347\231\273\345\275\225.png"
new file mode 100644
index 00000000..a4009b2f
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\347\224\250\346\210\267\347\231\273\345\275\225.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\212\225\347\250\277\346\216\245\345\217\243.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\212\225\347\250\277\346\216\245\345\217\243.png"
new file mode 100644
index 00000000..fe33abf9
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\212\225\347\250\277\346\216\245\345\217\243.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\265\201\346\216\245\345\217\243.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\265\201\346\216\245\345\217\243.png"
new file mode 100644
index 00000000..c27459e8
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\247\206\351\242\221\346\265\201\346\216\245\345\217\243.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.png" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.png"
new file mode 100644
index 00000000..78c5ad39
Binary files /dev/null and "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.png" differ
diff --git "a/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\347\244\276\344\272\244\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" "b/documents/01-\351\241\271\347\233\256\351\234\200\346\261\202/\344\270\232\345\212\241\346\265\201\347\250\213\346\226\207\346\241\243/\347\244\276\344\272\244\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep"
new file mode 100644
index 00000000..e69de29b
diff --git "a/documents/02-\347\274\226\347\240\201\350\247\204\350\214\203/README.md" "b/documents/02-\347\274\226\347\240\201\350\247\204\350\214\203/README.md"
new file mode 100644
index 00000000..570cdc2f
--- /dev/null
+++ "b/documents/02-\347\274\226\347\240\201\350\247\204\350\214\203/README.md"
@@ -0,0 +1,3749 @@
+## [uber-go/guide](https://github.com/uber-go/guide) 的中文翻译
+
+## [English](https://github.com/uber-go/guide/blob/master/style.md)
+
+## Uber Go 语言编码规范
+
+ [Uber](https://www.uber.com/) 是一家美国硅谷的科技公司,也是 Go 语言的早期 adopter。其开源了很多 golang 项目,诸如被 Gopher 圈熟知的 [zap](https://github.com/uber-go/zap)、[jaeger](https://github.com/jaegertracing/jaeger) 等。2018 年年末 Uber 将内部的 [Go 风格规范](https://github.com/uber-go/guide) 开源到 GitHub,经过一年的积累和更新,该规范已经初具规模,并受到广大 Gopher 的关注。本文是该规范的中文版本。本版本会根据原版实时更新。
+
+ ## 版本
+
+ - 当前更新版本:2022-04-25 版本地址:[commit:#180](https://github.com/uber-go/guide/commit/0bfd9f1f2483979ac70505e92e89057e2283e1b6)
+ - 如果您发现任何更新、问题或改进,请随时 fork 和 PR
+ - Please feel free to fork and PR if you find any updates, issues or improvement.
+
+## 目录
+
+- [uber-go/guide 的中文翻译](#uber-goguide-的中文翻译)
+- [English](#english)
+- [Uber Go 语言编码规范](#uber-go-语言编码规范)
+- [版本](#版本)
+- [目录](#目录)
+- [介绍](#介绍)
+- [指导原则](#指导原则)
+ - [指向 interface 的指针](#指向-interface-的指针)
+ - [Interface 合理性验证](#interface-合理性验证)
+ - [接收器 (receiver) 与接口](#接收器-receiver-与接口)
+ - [零值 Mutex 是有效的](#零值-mutex-是有效的)
+ - [在边界处拷贝 Slices 和 Maps](#在边界处拷贝-slices-和-maps)
+ - [接收 Slices 和 Maps](#接收-slices-和-maps)
+ - [返回 slices 或 maps](#返回-slices-或-maps)
+ - [使用 defer 释放资源](#使用-defer-释放资源)
+ - [Channel 的 size 要么是 1,要么是无缓冲的](#channel-的-size-要么是-1要么是无缓冲的)
+ - [枚举从 1 开始](#枚举从-1-开始)
+ - [使用 time 处理时间](#使用-time-处理时间)
+ - [使用 `time.Time` 表达瞬时时间](#使用-timetime-表达瞬时时间)
+ - [使用 `time.Duration` 表达时间段](#使用-timeduration-表达时间段)
+ - [对外部系统使用 `time.Time` 和 `time.Duration`](#对外部系统使用-timetime-和-timeduration)
+ - [Errors](#errors)
+ - [错误类型](#错误类型)
+ - [错误包装](#错误包装)
+ - [错误命名](#错误命名)
+ - [处理断言失败](#处理断言失败)
+ - [不要使用 panic](#不要使用-panic)
+ - [使用 go.uber.org/atomic](#使用-gouberorgatomic)
+ - [避免可变全局变量](#避免可变全局变量)
+ - [避免在公共结构中嵌入类型](#避免在公共结构中嵌入类型)
+ - [避免使用内置名称](#避免使用内置名称)
+ - [避免使用 `init()`](#避免使用-init)
+ - [追加时优先指定切片容量](#追加时优先指定切片容量)
+ - [主函数退出方式 (Exit)](#主函数退出方式-exit)
+ - [一次性退出](#一次性退出)
+ - [在序列化结构中使用字段标记](#在序列化结构中使用字段标记)
+ - [不要一劳永逸地使用 goroutine](#不要一劳永逸地使用-goroutine)
+ - [等待 goroutines 退出](#等待-goroutines-退出)
+ - [不要在 `init()` 使用 goroutines](#不要在-init-使用-goroutines)
+- [性能](#性能)
+ - [优先使用 strconv 而不是 fmt](#优先使用-strconv-而不是-fmt)
+ - [避免字符串到字节的转换](#避免字符串到字节的转换)
+ - [指定容器容量](#指定容器容量)
+ - [指定 Map 容量提示](#指定-map-容量提示)
+ - [指定切片容量](#指定切片容量)
+- [规范](#规范)
+ - [避免过长的行](#避免过长的行)
+ - [一致性](#一致性)
+ - [相似的声明放在一组](#相似的声明放在一组)
+ - [import 分组](#import-分组)
+ - [包名](#包名)
+ - [函数名](#函数名)
+ - [导入别名](#导入别名)
+ - [函数分组与顺序](#函数分组与顺序)
+ - [减少嵌套](#减少嵌套)
+ - [不必要的 else](#不必要的-else)
+ - [顶层变量声明](#顶层变量声明)
+ - [对于未导出的顶层常量和变量,使用_作为前缀](#对于未导出的顶层常量和变量使用_作为前缀)
+ - [结构体中的嵌入](#结构体中的嵌入)
+ - [本地变量声明](#本地变量声明)
+ - [nil 是一个有效的 slice](#nil-是一个有效的-slice)
+ - [缩小变量作用域](#缩小变量作用域)
+ - [避免参数语义不明确 (Avoid Naked Parameters)](#避免参数语义不明确-avoid-naked-parameters)
+ - [使用原始字符串字面值,避免转义](#使用原始字符串字面值避免转义)
+ - [初始化结构体](#初始化结构体)
+ - [使用字段名初始化结构](#使用字段名初始化结构)
+ - [省略结构中的零值字段](#省略结构中的零值字段)
+ - [对零值结构使用 `var`](#对零值结构使用-var)
+ - [初始化 Struct 引用](#初始化-struct-引用)
+ - [初始化 Maps](#初始化-maps)
+ - [字符串 string format](#字符串-string-format)
+ - [命名 Printf 样式的函数](#命名-printf-样式的函数)
+- [编程模式](#编程模式)
+ - [表驱动测试](#表驱动测试)
+ - [功能选项](#功能选项)
+- [Linting](#linting)
+ - [Lint Runners](#lint-runners)
+- [Stargazers over time](#stargazers-over-time)
+
+## 介绍
+
+样式 (style) 是支配我们代码的惯例。术语`样式`有点用词不当,因为这些约定涵盖的范围不限于由 gofmt 替我们处理的源文件格式。
+
+本指南的目的是通过详细描述在 Uber 编写 Go 代码的注意事项来管理这种复杂性。这些规则的存在是为了使代码库易于管理,同时仍然允许工程师更有效地使用 Go 语言功能。
+
+该指南最初由 [Prashant Varanasi] 和 [Simon Newton] 编写,目的是使一些同事能快速使用 Go。多年来,该指南已根据其他人的反馈进行了修改。
+
+[Prashant Varanasi]: https://github.com/prashantv
+[Simon Newton]: https://github.com/nomis52
+
+本文档记录了我们在 Uber 遵循的 Go 代码中的惯用约定。其中许多是 Go 的通用准则,而其他扩展准则依赖于下面外部的指南:
+
+1. [Effective Go](https://golang.org/doc/effective_go.html)
+2. [Go Common Mistakes](https://github.com/golang/go/wiki/CommonMistakes)
+3. [Go Code Review Comments](https://github.com/golang/go/wiki/CodeReviewComments)
+
+我们的目标是使代码示例能够准确地用于Go的两个发布版本 [releases](https://go.dev/doc/devel/release).
+
+所有代码都应该通过`golint`和`go vet`的检查并无错误。我们建议您将编辑器设置为:
+
+- 保存时运行 `goimports`
+- 运行 `golint` 和 `go vet` 检查错误
+
+您可以在以下 Go 编辑器工具支持页面中找到更为详细的信息:
+| Bad | Good |
|---|---|
| + +```go +// 如果 Handler 没有实现 http.Handler,会在运行时报错 +type Handler struct { + // ... +} +func (h *Handler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + ... +} +``` + + | + +```go +type Handler struct { + // ... +} +// 用于触发编译期的接口的合理性检查机制 +// 如果 Handler 没有实现 http.Handler,会在编译期报错 +var _ http.Handler = (*Handler)(nil) +func (h *Handler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + // ... +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +mu := new(sync.Mutex) +mu.Lock() +``` + + | + +```go +var mu sync.Mutex +mu.Lock() +``` + + |
| Bad | Good |
|---|---|
| + +```go +type SMap struct { + sync.Mutex + + data map[string]string +} + +func NewSMap() *SMap { + return &SMap{ + data: make(map[string]string), + } +} + +func (m *SMap) Get(k string) string { + m.Lock() + defer m.Unlock() + + return m.data[k] +} +``` + + | + +```go +type SMap struct { + mu sync.Mutex + + data map[string]string +} + +func NewSMap() *SMap { + return &SMap{ + data: make(map[string]string), + } +} + +func (m *SMap) Get(k string) string { + m.mu.Lock() + defer m.mu.Unlock() + + return m.data[k] +} +``` + + |
| + +`Mutex` 字段, `Lock` 和 `Unlock` 方法是 `SMap` 导出的 API 中不刻意说明的一部分。 + + | + +mutex 及其方法是 `SMap` 的实现细节,对其调用者不可见。 + + |
| Bad | Good |
|---|---|
| + +```go +func (d *Driver) SetTrips(trips []Trip) { + d.trips = trips +} + +trips := ... +d1.SetTrips(trips) + +// 你是要修改 d1.trips 吗? +trips[0] = ... +``` + + | ++ +```go +func (d *Driver) SetTrips(trips []Trip) { + d.trips = make([]Trip, len(trips)) + copy(d.trips, trips) +} + +trips := ... +d1.SetTrips(trips) + +// 这里我们修改 trips[0],但不会影响到 d1.trips +trips[0] = ... +``` + + | +
| Bad | Good |
|---|---|
| + +```go +type Stats struct { + mu sync.Mutex + + counters map[string]int +} + +// Snapshot 返回当前状态。 +func (s *Stats) Snapshot() map[string]int { + s.mu.Lock() + defer s.mu.Unlock() + + return s.counters +} + +// snapshot 不再受互斥锁保护 +// 因此对 snapshot 的任何访问都将受到数据竞争的影响 +// 影响 stats.counters +snapshot := stats.Snapshot() +``` + + | + +```go +type Stats struct { + mu sync.Mutex + + counters map[string]int +} + +func (s *Stats) Snapshot() map[string]int { + s.mu.Lock() + defer s.mu.Unlock() + + result := make(map[string]int, len(s.counters)) + for k, v := range s.counters { + result[k] = v + } + return result +} + +// snapshot 现在是一个拷贝 +snapshot := stats.Snapshot() +``` + + |
| Bad | Good |
|---|---|
| + +```go +p.Lock() +if p.count < 10 { + p.Unlock() + return p.count +} + +p.count++ +newCount := p.count +p.Unlock() + +return newCount + +// 当有多个 return 分支时,很容易遗忘 unlock +``` + + | + +```go +p.Lock() +defer p.Unlock() + +if p.count < 10 { + return p.count +} + +p.count++ +return p.count + +// 更可读 +``` + + |
| Bad | Good |
|---|---|
| + +```go +// 应该足以满足任何情况! +c := make(chan int, 64) +``` + + | + +```go +// 大小:1 +c := make(chan int, 1) // 或者 +// 无缓冲 channel,大小为 0 +c := make(chan int) +``` + + |
| Bad | Good |
|---|---|
| + +```go +type Operation int + +const ( + Add Operation = iota + Subtract + Multiply +) + +// Add=0, Subtract=1, Multiply=2 +``` + + | + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply +) + +// Add=1, Subtract=2, Multiply=3 +``` + + |
| Bad | Good |
|---|---|
| + +```go +func isActive(now, start, stop int) bool { + return start <= now && now < stop +} +``` + + | + +```go +func isActive(now, start, stop time.Time) bool { + return (start.Before(now) || start.Equal(now)) && now.Before(stop) +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +func poll(delay int) { + for { + // ... + time.Sleep(time.Duration(delay) * time.Millisecond) + } +} +poll(10) // 是几秒钟还是几毫秒? +``` + + | + +```go +func poll(delay time.Duration) { + for { + // ... + time.Sleep(delay) + } +} +poll(10*time.Second) +``` + + |
| Bad | Good |
|---|---|
| + +```go +// {"interval": 2} +type Config struct { + Interval int `json:"interval"` +} +``` + + | + +```go +// {"intervalMillis": 2000} +type Config struct { + IntervalMillis int `json:"intervalMillis"` +} +``` + + |
| 无错误匹配 | 错误匹配 |
|---|---|
| + +```go +// package foo + +func Open() error { + return errors.New("could not open") +} + +// package bar + +if err := foo.Open(); err != nil { + // Can't handle the error. + panic("unknown error") +} +``` + + | + +```go +// package foo + +var ErrCouldNotOpen = errors.New("could not open") + +func Open() error { + return ErrCouldNotOpen +} + +// package bar + +if err := foo.Open(); err != nil { + if errors.Is(err, foo.ErrCouldNotOpen) { + // handle the error + } else { + panic("unknown error") + } +} +``` + + |
| 无错误匹配 | 错误匹配 |
|---|---|
| + +```go +// package foo + +func Open(file string) error { + return fmt.Errorf("file %q not found", file) +} + +// package bar + +if err := foo.Open("testfile.txt"); err != nil { + // Can't handle the error. + panic("unknown error") +} +``` + + | + +```go +// package foo + +type NotFoundError struct { + File string +} + +func (e *NotFoundError) Error() string { + return fmt.Sprintf("file %q not found", e.File) +} + +func Open(file string) error { + return &NotFoundError{File: file} +} + + +// package bar + +if err := foo.Open("testfile.txt"); err != nil { + var notFound *NotFoundError + if errors.As(err, ¬Found) { + // handle the error + } else { + panic("unknown error") + } +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +s, err := store.New() +if err != nil { + return fmt.Errorf( + "failed to create new store: %w", err) +} +``` + + | + +```go +s, err := store.New() +if err != nil { + return fmt.Errorf( + "new store: %w", err) +} +``` + + |
| + +```plain +failed to x: failed to y: failed to create new store: the error +``` + + | + +```plain +x: y: new store: the error +``` + + |
| Description | Code |
|---|---|
| + +**Bad**: 记录错误并将其返回 + +堆栈中的调用程序可能会对该错误采取类似的操作。这样做会在应用程序日志中造成大量噪音,但收效甚微。 + + | + +```go +u, err := getUser(id) +if err != nil { + // BAD: See description + log.Printf("Could not get user %q: %v", id, err) + return err +} +``` + + |
| + +**Good**: 将错误换行并返回 + + + +堆栈中更靠上的调用程序将处理该错误。使用`%w`可确保它们可以将错误与`errors.Is`或`errors.As`相匹配 (如果相关)。 + + | + +```go +u, err := getUser(id) +if err != nil { + return fmt.Errorf("get user %q: %w", id, err) +} +``` + + |
| + +**Good**: 记录错误并正常降级 + +如果操作不是绝对必要的,我们可以通过从中恢复来提供降级但不间断的体验。 + + | + +```go +if err := emitMetrics(); err != nil { + // Failure to write metrics should not + // break the application. + log.Printf("Could not emit metrics: %v", err) +} + +``` + + |
| + +**Good**: 匹配错误并适当降级 + +如果被调用者在其约定中定义了一个特定的错误,并且失败是可恢复的,则匹配该错误案例并正常降级。对于所有其他案例,请包装错误并返回。 + +堆栈中更靠上的调用程序将处理其他错误。 + + | + +```go +tz, err := getUserTimeZone(id) +if err != nil { + if errors.Is(err, ErrUserNotFound) { + // User doesn't exist. Use UTC. + tz = time.UTC + } else { + return fmt.Errorf("get user %q: %w", id, err) + } +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +t := i.(string) +``` + + | + +```go +t, ok := i.(string) +if !ok { + // 优雅地处理错误 +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +func run(args []string) { + if len(args) == 0 { + panic("an argument is required") + } + // ... +} + +func main() { + run(os.Args[1:]) +} +``` + + | + +```go +func run(args []string) error { + if len(args) == 0 { + return errors.New("an argument is required") + } + // ... + return nil +} + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +// func TestFoo(t *testing.T) + +f, err := os.CreateTemp("", "test") +if err != nil { + panic("failed to set up test") +} +``` + + | + +```go +// func TestFoo(t *testing.T) + +f, err := os.CreateTemp("", "test") +if err != nil { + t.Fatal("failed to set up test") +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +type foo struct { + running int32 // atomic +} + +func (f* foo) start() { + if atomic.SwapInt32(&f.running, 1) == 1 { + // already running… + return + } + // start the Foo +} + +func (f *foo) isRunning() bool { + return f.running == 1 // race! +} +``` + + | + +```go +type foo struct { + running atomic.Bool +} + +func (f *foo) start() { + if f.running.Swap(true) { + // already running… + return + } + // start the Foo +} + +func (f *foo) isRunning() bool { + return f.running.Load() +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +// sign.go +var _timeNow = time.Now +func sign(msg string) string { + now := _timeNow() + return signWithTime(msg, now) +} +``` + + | + +```go +// sign.go +type signer struct { + now func() time.Time +} +func newSigner() *signer { + return &signer{ + now: time.Now, + } +} +func (s *signer) Sign(msg string) string { + now := s.now() + return signWithTime(msg, now) +} +``` + |
| + +```go +// sign_test.go +func TestSign(t *testing.T) { + oldTimeNow := _timeNow + _timeNow = func() time.Time { + return someFixedTime + } + defer func() { _timeNow = oldTimeNow }() + assert.Equal(t, want, sign(give)) +} +``` + + | + +```go +// sign_test.go +func TestSigner(t *testing.T) { + s := newSigner() + s.now = func() time.Time { + return someFixedTime + } + assert.Equal(t, want, s.Sign(give)) +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +// ConcreteList 是一个实体列表。 +type ConcreteList struct { + *AbstractList +} +``` + + | + +```go +// ConcreteList 是一个实体列表。 +type ConcreteList struct { + list *AbstractList +} +// 添加将实体添加到列表中。 +func (l *ConcreteList) Add(e Entity) { + l.list.Add(e) +} +// 移除从列表中移除实体。 +func (l *ConcreteList) Remove(e Entity) { + l.list.Remove(e) +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +// AbstractList 是各种实体列表的通用实现。 +type AbstractList interface { + Add(Entity) + Remove(Entity) +} +// ConcreteList 是一个实体列表。 +type ConcreteList struct { + AbstractList +} +``` + + | + +```go +// ConcreteList 是一个实体列表。 +type ConcreteList struct { + list AbstractList +} +// 添加将实体添加到列表中。 +func (l *ConcreteList) Add(e Entity) { + l.list.Add(e) +} +// 移除从列表中移除实体。 +func (l *ConcreteList) Remove(e Entity) { + l.list.Remove(e) +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +var error string +// `error` 作用域隐式覆盖 + +// or + +func handleErrorMessage(error string) { + // `error` 作用域隐式覆盖 +} +``` + + | + +```go +var errorMessage string +// `error` 指向内置的非覆盖 + +// or + +func handleErrorMessage(msg string) { + // `error` 指向内置的非覆盖 +} +``` + + |
| + +```go +type Foo struct { + // 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。 + error error + string string +} + +func (f Foo) Error() error { + // `error` 和 `f.error` 在视觉上是相似的 + return f.error +} + +func (f Foo) String() string { + // `string` and `f.string` 在视觉上是相似的 + return f.string +} +``` + + | + +```go +type Foo struct { + // `error` and `string` 现在是明确的。 + err error + str string +} + +func (f Foo) Error() error { + return f.err +} + +func (f Foo) String() string { + return f.str +} +``` + |
| Bad | Good |
|---|---|
| + +```go +type Foo struct { + // ... +} +var _defaultFoo Foo +func init() { + _defaultFoo = Foo{ + // ... + } +} +``` + + | + +```go +var _defaultFoo = Foo{ + // ... +} +// or,为了更好的可测试性: +var _defaultFoo = defaultFoo() +func defaultFoo() Foo { + return Foo{ + // ... + } +} +``` + + |
| + +```go +type Config struct { + // ... +} +var _config Config +func init() { + // Bad: 基于当前目录 + cwd, _ := os.Getwd() + // Bad: I/O + raw, _ := os.ReadFile( + path.Join(cwd, "config", "config.yaml"), + ) + yaml.Unmarshal(raw, &_config) +} +``` + + | + +```go +type Config struct { + // ... +} +func loadConfig() Config { + cwd, err := os.Getwd() + // handle err + raw, err := os.ReadFile( + path.Join(cwd, "config", "config.yaml"), + ) + // handle err + var config Config + yaml.Unmarshal(raw, &config) + return config +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + + | + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0, size) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + + |
| + +``` +BenchmarkBad-4 100000000 2.48s +``` + + | + +``` +BenchmarkGood-4 100000000 0.21s +``` + + |
| Bad | Good |
|---|---|
| + +```go +func main() { + body := readFile(path) + fmt.Println(body) +} +func readFile(path string) string { + f, err := os.Open(path) + if err != nil { + log.Fatal(err) + } + b, err := os.ReadAll(f) + if err != nil { + log.Fatal(err) + } + return string(b) +} +``` + + | + +```go +func main() { + body, err := readFile(path) + if err != nil { + log.Fatal(err) + } + fmt.Println(body) +} +func readFile(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + b, err := os.ReadAll(f) + if err != nil { + return "", err + } + return string(b), nil +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +package main +func main() { + args := os.Args[1:] + if len(args) != 1 { + log.Fatal("missing file") + } + name := args[0] + f, err := os.Open(name) + if err != nil { + log.Fatal(err) + } + defer f.Close() + // 如果我们调用 log.Fatal 在这条线之后 + // f.Close 将会被执行。 + b, err := os.ReadAll(f) + if err != nil { + log.Fatal(err) + } + // ... +} +``` + + | + +```go +package main +func main() { + if err := run(); err != nil { + log.Fatal(err) + } +} +func run() error { + args := os.Args[1:] + if len(args) != 1 { + return errors.New("missing file") + } + name := args[0] + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + b, err := os.ReadAll(f) + if err != nil { + return err + } + // ... +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +type Stock struct { + Price int + Name string +} +bytes, err := json.Marshal(Stock{ + Price: 137, + Name: "UBER", +}) +``` + + | + +```go +type Stock struct { + Price int `json:"price"` + Name string `json:"name"` + // Safe to rename Name to Symbol. +} +bytes, err := json.Marshal(Stock{ + Price: 137, + Name: "UBER", +}) +``` + + |
| Bad | Good |
|---|---|
| + +```go +go func() { + for { + flush() + time.Sleep(delay) + } +}() +``` + + | + +```go +var ( + stop = make(chan struct{}) // 告诉 goroutine 停止 + done = make(chan struct{}) // 告诉我们 goroutine 退出了 +) +go func() { + defer close(done) + ticker := time.NewTicker(delay) + defer ticker.Stop() + for { + select { + case <-tick.C: + flush() + case <-stop: + return + } + } +}() +// 其它... +close(stop) // 指示 goroutine 停止 +<-done // and wait for it to exit +``` + + |
| + +没有办法阻止这个 goroutine。这将一直运行到应用程序退出。 + + | + +这个 goroutine 可以用 `close(stop)`, +我们可以等待它退出 `<-done`. + + |
| Bad | Good |
|---|---|
| + +```go +func init() { + go doWork() +} +func doWork() { + for { + // ... + } +} +``` + + | + +```go +type Worker struct{ /* ... */ } +func NewWorker(...) *Worker { + w := &Worker{ + stop: make(chan struct{}), + done: make(chan struct{}), + // ... + } + go w.doWork() +} +func (w *Worker) doWork() { + defer close(w.done) + for { + // ... + case <-w.stop: + return + } +} +// Shutdown 告诉 worker 停止 +// 并等待它完成。 +func (w *Worker) Shutdown() { + close(w.stop) + <-w.done +} +``` + + |
| + +当用户导出这个包时,无条件地生成一个后台 goroutine。 +用户无法控制 goroutine 或停止它的方法。 + + | + +仅当用户请求时才生成工作人员。 +提供一种关闭工作器的方法,以便用户可以释放工作器使用的资源。 + +请注意,如果工作人员管理多个 goroutine,则应使用`WaitGroup`。 +请参阅 [等待 goroutines 退出](#等待-goroutines-退出)。 + + + |
| Bad | Good |
|---|---|
| + +```go +for i := 0; i < b.N; i++ { + s := fmt.Sprint(rand.Int()) +} +``` + + | + +```go +for i := 0; i < b.N; i++ { + s := strconv.Itoa(rand.Int()) +} +``` + + |
| + +```plain +BenchmarkFmtSprint-4 143 ns/op 2 allocs/op +``` + + | + +```plain +BenchmarkStrconv-4 64.2 ns/op 1 allocs/op +``` + + |
| Bad | Good |
|---|---|
| + +```go +for i := 0; i < b.N; i++ { + w.Write([]byte("Hello world")) +} +``` + + | + +```go +data := []byte("Hello world") +for i := 0; i < b.N; i++ { + w.Write(data) +} +``` + + |
| + +```plain +BenchmarkBad-4 50000000 22.2 ns/op +``` + + | + +```plain +BenchmarkGood-4 500000000 3.25 ns/op +``` + + |
| Bad | Good |
|---|---|
| + +```go +m := make(map[string]os.FileInfo) + +files, _ := os.ReadDir("./files") +for _, f := range files { + m[f.Name()] = f +} +``` + + | + +```go + +files, _ := os.ReadDir("./files") + +m := make(map[string]os.FileInfo, len(files)) +for _, f := range files { + m[f.Name()] = f +} +``` + + |
| + +`m` 是在没有大小提示的情况下创建的; 在运行时可能会有更多分配。 + + | + +`m` 是有大小提示创建的;在运行时可能会有更少的分配。 + + |
| Bad | Good |
|---|---|
| + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + + | + +```go +for n := 0; n < b.N; n++ { + data := make([]int, 0, size) + for k := 0; k < size; k++{ + data = append(data, k) + } +} +``` + + |
| + +``` +BenchmarkBad-4 100000000 2.48s +``` + + | + +``` +BenchmarkGood-4 100000000 0.21s +``` + + |
| Bad | Good |
|---|---|
| + +```go +import "a" +import "b" +``` + + | + +```go +import ( + "a" + "b" +) +``` + + |
| Bad | Good |
|---|---|
| + +```go + +const a = 1 +const b = 2 + +var a = 1 +var b = 2 + +type Area float64 +type Volume float64 +``` + + | + +```go +const ( + a = 1 + b = 2 +) + +var ( + a = 1 + b = 2 +) + +type ( + Area float64 + Volume float64 +) +``` + + |
| Bad | Good |
|---|---|
| + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply + EnvVar = "MY_ENV" +) +``` + + | + +```go +type Operation int + +const ( + Add Operation = iota + 1 + Subtract + Multiply +) + +const EnvVar = "MY_ENV" +``` + + |
| Bad | Good |
|---|---|
| + +```go +func f() string { + red := color.New(0xff0000) + green := color.New(0x00ff00) + blue := color.New(0x0000ff) + + ... +} +``` + + | + +```go +func f() string { + var ( + red = color.New(0xff0000) + green = color.New(0x00ff00) + blue = color.New(0x0000ff) + ) + + ... +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +func (c *client) request() { + caller := c.name + format := "json" + timeout := 5*time.Second + var err error + // ... +} +``` + + | + +```go +func (c *client) request() { + var ( + caller = c.name + format = "json" + timeout = 5*time.Second + err error + ) + // ... +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +import ( + "fmt" + "os" + "go.uber.org/atomic" + "golang.org/x/sync/errgroup" +) +``` + + | + +```go +import ( + "fmt" + "os" + + "go.uber.org/atomic" + "golang.org/x/sync/errgroup" +) +``` + + |
| Bad | Good |
|---|---|
| + +```go +import ( + "fmt" + "os" + + nettrace "golang.net/x/trace" +) +``` + + | + +```go +import ( + "fmt" + "os" + "runtime/trace" + + nettrace "golang.net/x/trace" +) +``` + + |
| Bad | Good |
|---|---|
| + +```go +func (s *something) Cost() { + return calcCost(s.weights) +} + +type something struct{ ... } + +func calcCost(n []int) int {...} + +func (s *something) Stop() {...} + +func newSomething() *something { + return &something{} +} +``` + + | + +```go +type something struct{ ... } + +func newSomething() *something { + return &something{} +} + +func (s *something) Cost() { + return calcCost(s.weights) +} + +func (s *something) Stop() {...} + +func calcCost(n []int) int {...} +``` + + |
| Bad | Good |
|---|---|
| + +```go +for _, v := range data { + if v.F1 == 1 { + v = process(v) + if err := v.Call(); err == nil { + v.Send() + } else { + return err + } + } else { + log.Printf("Invalid v: %v", v) + } +} +``` + + | + +```go +for _, v := range data { + if v.F1 != 1 { + log.Printf("Invalid v: %v", v) + continue + } + + v = process(v) + if err := v.Call(); err != nil { + return err + } + v.Send() +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +var a int +if b { + a = 100 +} else { + a = 10 +} +``` + + | + +```go +a := 10 +if b { + a = 100 +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +var _s string = F() + +func F() string { return "A" } +``` + + | + +```go +var _s = F() +// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型 +// 还是那种类型 + +func F() string { return "A" } +``` + + |
| Bad | Good |
|---|---|
| + +```go +// foo.go + +const ( + defaultPort = 8080 + defaultUser = "user" +) + +// bar.go + +func Bar() { + defaultPort := 9090 + ... + fmt.Println("Default port", defaultPort) + + // We will not see a compile error if the first line of + // Bar() is deleted. +} +``` + + | + +```go +// foo.go + +const ( + _defaultPort = 8080 + _defaultUser = "user" +) +``` + + |
| Bad | Good |
|---|---|
| + +```go +type Client struct { + version int + http.Client +} +``` + + | + +```go +type Client struct { + http.Client + + version int +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +type A struct { + // Bad: A.Lock() and A.Unlock() 现在可用 + // 不提供任何功能性好处,并允许用户控制有关 A 的内部细节。 + sync.Mutex +} +``` + + | + +```go +type countingWriteCloser struct { + // Good: Write() 在外层提供用于特定目的, + // 并且委托工作到内部类型的 Write() 中。 + io.WriteCloser + count int +} +func (w *countingWriteCloser) Write(bs []byte) (int, error) { + w.count += len(bs) + return w.WriteCloser.Write(bs) +} +``` + + |
| + +```go +type Book struct { + // Bad: 指针更改零值的有用性 + io.ReadWriter + // other fields +} +// later +var b Book +b.Read(...) // panic: nil pointer +b.String() // panic: nil pointer +b.Write(...) // panic: nil pointer +``` + + | + +```go +type Book struct { + // Good: 有用的零值 + bytes.Buffer + // other fields +} +// later +var b Book +b.Read(...) // ok +b.String() // ok +b.Write(...) // ok +``` + + |
| + +```go +type Client struct { + sync.Mutex + sync.WaitGroup + bytes.Buffer + url.URL +} +``` + + | + +```go +type Client struct { + mtx sync.Mutex + wg sync.WaitGroup + buf bytes.Buffer + url url.URL +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +var s = "foo" +``` + + | + +```go +s := "foo" +``` + + |
| Bad | Good |
|---|---|
| + +```go +func f(list []int) { + filtered := []int{} + for _, v := range list { + if v > 10 { + filtered = append(filtered, v) + } + } +} +``` + + | + +```go +func f(list []int) { + var filtered []int + for _, v := range list { + if v > 10 { + filtered = append(filtered, v) + } + } +} +``` + + |
| Bad | Good |
|---|---|
| + + ```go + if x == "" { + return []int{} + } + ``` + + | + + ```go + if x == "" { + return nil + } + ``` + + |
| Bad | Good |
|---|---|
| + + ```go + func isEmpty(s []string) bool { + return s == nil + } + ``` + + | + + ```go + func isEmpty(s []string) bool { + return len(s) == 0 + } + ``` + + |
| Bad | Good |
|---|---|
| + + ```go + nums := []int{} + // or, nums := make([]int) + + if add1 { + nums = append(nums, 1) + } + + if add2 { + nums = append(nums, 2) + } + ``` + + | + + ```go + var nums []int + + if add1 { + nums = append(nums, 1) + } + + if add2 { + nums = append(nums, 2) + } + ``` + + |
| Bad | Good |
|---|---|
| + +```go +err := os.WriteFile(name, data, 0644) +if err != nil { + return err +} +``` + + | + +```go +if err := os.WriteFile(name, data, 0644); err != nil { + return err +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +if data, err := os.ReadFile(name); err == nil { + err = cfg.Decode(data) + if err != nil { + return err + } + + fmt.Println(cfg) + return nil +} else { + return err +} +``` + + | + +```go +data, err := os.ReadFile(name) +if err != nil { + return err +} + +if err := cfg.Decode(data); err != nil { + return err +} + +fmt.Println(cfg) +return nil +``` + + |
| Bad | Good |
|---|---|
| + +```go +// func printInfo(name string, isLocal, done bool) + +printInfo("foo", true, true) +``` + + | + +```go +// func printInfo(name string, isLocal, done bool) + +printInfo("foo", true /* isLocal */, true /* done */) +``` + + |
| Bad | Good |
|---|---|
| + +```go +wantError := "unknown name:\"test\"" +``` + + | + +```go +wantError := `unknown error:"test"` +``` + + |
| Bad | Good |
|---|---|
| + +```go +k := User{"John", "Doe", true} +``` + + | + +```go +k := User{ + FirstName: "John", + LastName: "Doe", + Admin: true, +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +user := User{ + FirstName: "John", + LastName: "Doe", + MiddleName: "", + Admin: false, +} +``` + + | + +```go +user := User{ + FirstName: "John", + LastName: "Doe", +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +user := User{} +``` + + | + +```go +var user User +``` + + |
| Bad | Good |
|---|---|
| + +```go +sval := T{Name: "foo"} + +// inconsistent +sptr := new(T) +sptr.Name = "bar" +``` + + | + +```go +sval := T{Name: "foo"} + +sptr := &T{Name: "bar"} +``` + + |
| Bad | Good |
|---|---|
| + +```go +var ( + // m1 读写安全; + // m2 在写入时会 panic + m1 = map[T1]T2{} + m2 map[T1]T2 +) +``` + + | + +```go +var ( + // m1 读写安全; + // m2 在写入时会 panic + m1 = make(map[T1]T2) + m2 map[T1]T2 +) +``` + + |
| + +声明和初始化看起来非常相似的。 + + | + +声明和初始化看起来差别非常大。 + + |
| Bad | Good |
|---|---|
| + +```go +m := make(map[T1]T2, 3) +m[k1] = v1 +m[k2] = v2 +m[k3] = v3 +``` + + | + +```go +m := map[T1]T2{ + k1: v1, + k2: v2, + k3: v3, +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +msg := "unexpected values %v, %v\n" +fmt.Printf(msg, 1, 2) +``` + + | + +```go +const msg = "unexpected values %v, %v\n" +fmt.Printf(msg, 1, 2) +``` + + |
| Bad | Good |
|---|---|
| + +```go +// func TestSplitHostPort(t *testing.T) + +host, port, err := net.SplitHostPort("192.0.2.0:8000") +require.NoError(t, err) +assert.Equal(t, "192.0.2.0", host) +assert.Equal(t, "8000", port) + +host, port, err = net.SplitHostPort("192.0.2.0:http") +require.NoError(t, err) +assert.Equal(t, "192.0.2.0", host) +assert.Equal(t, "http", port) + +host, port, err = net.SplitHostPort(":8000") +require.NoError(t, err) +assert.Equal(t, "", host) +assert.Equal(t, "8000", port) + +host, port, err = net.SplitHostPort("1:8") +require.NoError(t, err) +assert.Equal(t, "1", host) +assert.Equal(t, "8", port) +``` + + | + +```go +// func TestSplitHostPort(t *testing.T) + +tests := []struct{ + give string + wantHost string + wantPort string +}{ + { + give: "192.0.2.0:8000", + wantHost: "192.0.2.0", + wantPort: "8000", + }, + { + give: "192.0.2.0:http", + wantHost: "192.0.2.0", + wantPort: "http", + }, + { + give: ":8000", + wantHost: "", + wantPort: "8000", + }, + { + give: "1:8", + wantHost: "1", + wantPort: "8", + }, +} + +for _, tt := range tests { + t.Run(tt.give, func(t *testing.T) { + host, port, err := net.SplitHostPort(tt.give) + require.NoError(t, err) + assert.Equal(t, tt.wantHost, host) + assert.Equal(t, tt.wantPort, port) + }) +} +``` + + |
| Bad | Good |
|---|---|
| + +```go +// package db + +func Open( + addr string, + cache bool, + logger *zap.Logger +) (*Connection, error) { + // ... +} +``` + + | + +```go +// package db + +type Option interface { + // ... +} + +func WithCache(c bool) Option { + // ... +} + +func WithLogger(log *zap.Logger) Option { + // ... +} + +// Open creates a connection. +func Open( + addr string, + opts ...Option, +) (*Connection, error) { + // ... +} +``` + + |
| + +必须始终提供缓存和记录器参数,即使用户希望使用默认值。 + +```go +db.Open(addr, db.DefaultCache, zap.NewNop()) +db.Open(addr, db.DefaultCache, log) +db.Open(addr, false /* cache */, zap.NewNop()) +db.Open(addr, false /* cache */, log) +``` + + | + +只有在需要时才提供选项。 + +```go +db.Open(addr) +db.Open(addr, db.WithLogger(log)) +db.Open(addr, db.WithCache(false)) +db.Open( + addr, + db.WithCache(false), + db.WithLogger(log), +) +``` + + |