diff --git a/.gitignore b/.gitignore index 82c4ca1c..1a41a2b8 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ # vendor/ # Go workspace file -go.work +**/go.work +**/go.sum -.idea \ No newline at end of file +# goland IDE file +**/.idea/ diff --git a/Go-Project/.gitignore b/Go-Project/.gitignore new file mode 100644 index 00000000..ef2558d5 --- /dev/null +++ b/Go-Project/.gitignore @@ -0,0 +1,2 @@ +# video files +/public/ \ No newline at end of file diff --git a/Go-Project/config.yaml b/Go-Project/config.yaml new file mode 100644 index 00000000..62b75a66 --- /dev/null +++ b/Go-Project/config.yaml @@ -0,0 +1,9 @@ +mysql: + path: 47.113.185.186 + port: 3306 + config: charset=utf8&parseTime=True&loc=Local + db-name: douyin_simple + username: root + password: 11LvmIa1wz2tMI5P + max-idle-conns: 10 + max-open-conns: 100 \ No newline at end of file diff --git a/Go-Project/config/config.go b/Go-Project/config/config.go new file mode 100644 index 00000000..9e986ee1 --- /dev/null +++ b/Go-Project/config/config.go @@ -0,0 +1,6 @@ +package config + +type Server struct { + Mysql Mysql `mapstructure:"mysql" yaml:"mysql"` + //接下来Redis这些也在这相似地定义 +} diff --git a/Go-Project/config/mysql.go b/Go-Project/config/mysql.go new file mode 100644 index 00000000..393baaa7 --- /dev/null +++ b/Go-Project/config/mysql.go @@ -0,0 +1,16 @@ +package config + +type Mysql struct { + Path string `mapstructure:"path" yaml:"path"` // 服务器地址 + Port string `mapstructure:"port" yaml:"port"` // 端口 + Config string `mapstructure:"config" yaml:"config"` // 高级配置 + Dbname string `mapstructure:"db-name" yaml:"db-name"` // 数据库名 + Username string `mapstructure:"username" yaml:"username"` // 数据库用户名 + Password string `mapstructure:"password" yaml:"password"` // 数据库密码 + MaxIdleConns int `mapstructure:"max-idle-conns" yaml:"max-idle-conns"` // 空闲中的最大连接数 + MaxOpenConns int `mapstructure:"max-open-conns" yaml:"max-open-conns"` // 打开到数据库的最大连接数 +} + +func (m *Mysql) Dsn() string { + return m.Username + ":" + m.Password + "@tcp(" + m.Path + ":" + m.Port + ")/" + m.Dbname + "?" + m.Config +} diff --git a/Go-Project/controller/comment.go b/Go-Project/controller/comment.go new file mode 100644 index 00000000..5cb5fbc8 --- /dev/null +++ b/Go-Project/controller/comment.go @@ -0,0 +1,75 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/response" + "github.com/life-studied/douyin-simple/service" + "net/http" + "strconv" +) + +// CommentAction no practical effect, just check if token is valid +func CommentAction(c *gin.Context) { + // 获取请求参数 + videoID, err := strconv.ParseInt(c.Query("video_id"), 10, 64) + if err != nil { + // 处理videoID解析错误 + c.JSON(http.StatusBadRequest, response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusBadRequest, StatusMsg: "无效的video_id"}, + }) + return + } + actionType := c.Query("action_type") + commentText := c.Query("comment_text") + + token := c.Query("token") + user, exists := usersLoginInfo[token] + if !exists { + c.JSON(http.StatusBadRequest, response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusBadRequest, StatusMsg: "无效的token"}, + }) + return + } + userID := user.Id + + // 判断操作类型 + if actionType == "1" { + commentActionResponse, _ := service.CreateComment(userID, videoID, commentText) + c.JSON(http.StatusOK, commentActionResponse) + + } else if actionType == "2" { + commentID, err := strconv.ParseInt(c.Query("comment_id"), 10, 64) + if err != nil { + // 处理commentID解析错误 + c.JSON(http.StatusBadRequest, response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusBadRequest, StatusMsg: "无效的comment_id"}, + }) + return + } + commentActionResponse, _ := service.DeleteComment(userID, videoID, commentID) + c.JSON(http.StatusOK, commentActionResponse) + } + + return +} + +func CommentList(c *gin.Context) { + videoID, err := strconv.ParseInt(c.Query("video_id"), 10, 64) + if err != nil { + // 处理videoID解析错误 + c.JSON(http.StatusBadRequest, response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusBadRequest, StatusMsg: "无效的video_id"}, + }) + return + } + + // 将获取到的评论添加到commentList列表中 + + comments, err := service.GetCommentList(videoID) + // 返回response + c.JSON(http.StatusOK, response.CommentListResponse{ + Response: response.Response{StatusCode: 0, StatusMsg: "OK"}, + CommentList: comments, + }) + return +} diff --git a/controller/common.go b/Go-Project/controller/common.go similarity index 95% rename from controller/common.go rename to Go-Project/controller/common.go index 613cd6e7..8dae9998 100644 --- a/controller/common.go +++ b/Go-Project/controller/common.go @@ -17,7 +17,7 @@ type Video struct { type Comment struct { Id int64 `json:"id,omitempty"` - User User `json:"user"` + User User `json:"user" gorm:"foreignKey:user_id;references:id;"` Content string `json:"content,omitempty"` CreateDate string `json:"create_date,omitempty"` } diff --git a/Go-Project/controller/demo_data.go b/Go-Project/controller/demo_data.go new file mode 100644 index 00000000..f74b99e5 --- /dev/null +++ b/Go-Project/controller/demo_data.go @@ -0,0 +1,12 @@ +package controller + +var DemoVideos []Video + +var DemoComments = []Comment{ + { + Id: 1, + User: usersLoginInfo["user_1password_1"], + Content: "Test Comment", + CreateDate: "05-01", + }, +} diff --git a/Go-Project/controller/favorite.go b/Go-Project/controller/favorite.go new file mode 100644 index 00000000..affccde1 --- /dev/null +++ b/Go-Project/controller/favorite.go @@ -0,0 +1,156 @@ +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/service" + "net/http" +) + +// FavoriteAction no practical effect, just check if token is valid +func FavoriteAction(c *gin.Context) { + //接收参数,并判断是否合法 + token, tokenOk := c.GetQuery("token") + if !tokenOk { + c.JSON(http.StatusBadRequest, Response{StatusCode: 400, StatusMsg: "Lack of token"}) + return + } + userFromToken, exist := usersLoginInfo[token] + if !exist { + c.JSON(http.StatusUnprocessableEntity, Response{StatusCode: 422, StatusMsg: "Token is invalid"}) + return + } + + videoId, videoIdOk := c.GetQuery("video_id") + if !videoIdOk { + c.JSON(http.StatusBadRequest, Response{StatusCode: 400, StatusMsg: "Lack of video_id"}) + return + } + actionType, actionTypeOk := c.GetQuery("action_type") + if !actionTypeOk { + c.JSON(http.StatusBadRequest, Response{StatusCode: 400, StatusMsg: "Lack of action_type"}) + return + } + + //判断操作类型 + var video = service.FavoriteVideoID{ + VideoID: videoId, + } + var user = service.FavoriteUserID{ + UserID: userFromToken.Id, + UserName: userFromToken.Name, + } + if actionType == "1" { + err := service.FavoriteVideo(video, user) + if err != nil { + c.JSON(http.StatusInternalServerError, Response{StatusCode: 500, StatusMsg: "Favorite failed"}) + //打印报错 + fmt.Printf("\033[1;37;41m%s\033[0m\n", "|BUG | "+err.Error()+"form Favorite") + return + } + + } else if actionType == "2" { + err := service.UnfavoriteVideo(video, user) + if err != nil { + c.JSON(http.StatusInternalServerError, Response{StatusCode: 500, StatusMsg: "Unfavorite failed"}) + //打印报错 + fmt.Printf("\033[1;37;41m%s\033[0m\n", "|BUG | "+err.Error()+"form Favorite") + return + } + } else { + c.JSON(http.StatusUnprocessableEntity, Response{StatusCode: 422, StatusMsg: "Invalid action_type"}) + return + } + c.JSON(http.StatusOK, Response{StatusCode: 0, StatusMsg: "Success"}) +} + +// FavoriteList all users have same favorite video list +func FavoriteList(c *gin.Context) { + //接收参数,并判断是否合法 + token, tokenOk := c.GetQuery("token") + if !tokenOk { + c.JSON(http.StatusBadRequest, VideoListResponse{ + Response: Response{ + StatusCode: 400, + StatusMsg: "Lack of token", + }, + VideoList: nil, + }) + return + } + //检测token + userFromToken, exist := usersLoginInfo[token] + if !exist { + c.JSON(http.StatusUnprocessableEntity, VideoListResponse{ + Response: Response{ + StatusCode: 422, + StatusMsg: "Token is invalid", + }, + VideoList: nil, + }) + return + } + //转为service层使用的结构体类型 + user := service.FavoriteUserID{ + UserID: userFromToken.Id, + UserName: userFromToken.Name, + } + //用不到但是必须要有的接收参数 + _, userIdOk := c.GetQuery("user_id") + if !userIdOk { + c.JSON(http.StatusBadRequest, VideoListResponse{ + Response: Response{ + StatusCode: 400, + StatusMsg: "Lack of user_id", + }, + VideoList: nil, + }) + return + } + //获取用户收藏的视频列表 + videoList, err := service.ReadFavoriteVideo(user) + if err != nil { + fmt.Printf("\033[1;37;41m%s\033[0m\n", "| BUG | "+err.Error()+"form Favorite") + c.JSON(http.StatusInternalServerError, VideoListResponse{ + Response: Response{ + StatusCode: 500, + StatusMsg: "Internal server error", + }, + VideoList: nil, + }) + return + } + //转为前端需要的结构体类型 + c.JSON(http.StatusOK, serviceToVideoList(videoList)) +} + +func serviceToVideoList(favoriteVideoList []service.FavoriteVideoList) (response VideoListResponse) { + + response.Response = Response{ + StatusCode: 0, + StatusMsg: "Success", + } + response.VideoList = make([]Video, 0, len(favoriteVideoList)) + for _, favoriteVideo := range favoriteVideoList { + response.VideoList = append(response.VideoList, Video{ + Id: favoriteVideo.Id, + Author: serviceToUser(favoriteVideo.Author), + PlayUrl: favoriteVideo.PlayUrl, + CoverUrl: favoriteVideo.CoverUrl, + FavoriteCount: favoriteVideo.FavoriteCount, + CommentCount: favoriteVideo.CommentCount, + IsFavorite: true, + }) + } + return response +} + +func serviceToUser(favoriteUser service.FavoriteUserID) (user User) { + user = User{ + Id: favoriteUser.UserID, + Name: favoriteUser.UserName, + FollowCount: favoriteUser.FollowCount, + FollowerCount: favoriteUser.FollowerCount, + } + return user +} diff --git a/Go-Project/controller/feed.go b/Go-Project/controller/feed.go new file mode 100644 index 00000000..d6f18712 --- /dev/null +++ b/Go-Project/controller/feed.go @@ -0,0 +1,37 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/service" + "net/http" +) + +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) { + strLatestTime, exists := c.GetQuery("latest_time") + if !exists { + c.JSON(http.StatusBadRequest, FeedResponse{Response: Response{StatusCode: 1, StatusMsg: "latest_time is empty"}}) + return + } + nextTime, startId, err := service.GetNextTime(strLatestTime) + if err != nil { + c.JSON(http.StatusBadRequest, FeedResponse{Response: Response{StatusCode: 1, StatusMsg: err.Error()}}) + return + } + var endId int64 + if startId+30 > int64(len(DemoVideos)) { + endId = int64(len(DemoVideos)) + } else { + endId = startId + 30 + } + c.JSON(http.StatusOK, FeedResponse{Response: Response{StatusCode: 0}, + VideoList: DemoVideos[startId:endId], + NextTime: nextTime, + }) +} diff --git a/Go-Project/controller/init_demo_data.go b/Go-Project/controller/init_demo_data.go new file mode 100644 index 00000000..ec5cdf5e --- /dev/null +++ b/Go-Project/controller/init_demo_data.go @@ -0,0 +1,78 @@ +// Package controller ----------------------------- +// @file : init_demo_data.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/28 23:42 +// ------------------------------------------- +package controller + +import ( + "fmt" + "github.com/life-studied/douyin-simple/dao" + "os" +) + +func InitCacheFromMysql() { + InitUserFromMysql() + InitVideoDataFromMysql() +} + +func InitUserFromMysql() { + users, err := dao.InitUserFromMysql() + if err != nil { + fmt.Println("Init fail:InitUser function failed") + os.Exit(1) + } + for i := 0; i < len(users); i++ { + token := users[i].Name + users[i].Password + usersLoginInfo[token] = User{ + Id: users[i].ID, + Name: users[i].Name, + FollowCount: 0, + FollowerCount: 0, + IsFollow: false, + } + } +} + +func InitVideoDataFromMysql() { + videos, err := dao.InitVideoDataFromMysql() + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + for i := 0; i < len(videos); i++ { + name, password, err := dao.InitUserByUserIdFromMysql(videos[i].AuthorID) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + token := name + password + user, exists := usersLoginInfo[token] + if !exists { + fmt.Println("init fail:usersLoginInfo don't have user from mysql, please check your initUser function") + os.Exit(1) + } + + favoriteCount, err := dao.InitVideoFavoriteCountFromMysql(videos[i].ID) + if err != nil { + fmt.Println("Init fail: Video favorite count init failed") + os.Exit(1) + } + + commentCount, err := dao.InitVideoCommentCountFromMysql(videos[i].ID) + if err != nil { + fmt.Println("Init fail: Video favorite count init failed") + os.Exit(1) + } + DemoVideos = append(DemoVideos, Video{ + Id: videos[i].ID, + Author: user, + PlayUrl: videos[i].PlayURL, + CoverUrl: videos[i].CoverURL, + FavoriteCount: favoriteCount, + CommentCount: commentCount, + IsFavorite: false, + }) + } +} diff --git a/controller/message.go b/Go-Project/controller/message.go similarity index 87% rename from controller/message.go rename to Go-Project/controller/message.go index f4e4e4f5..06f55ee8 100644 --- a/controller/message.go +++ b/Go-Project/controller/message.go @@ -3,19 +3,20 @@ package controller import ( "fmt" "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/service" "net/http" "strconv" "sync/atomic" "time" ) -var tempChat = map[string][]Message{} +var tempChat = map[string][]service.Message{} var messageIdSequence = int64(1) type ChatResponse struct { Response - MessageList []Message `json:"message_list"` + MessageList []service.Message `json:"message_list"` } // MessageAction no practical effect, just check if token is valid @@ -29,7 +30,7 @@ func MessageAction(c *gin.Context) { chatKey := genChatKey(user.Id, int64(userIdB)) atomic.AddInt64(&messageIdSequence, 1) - curMessage := Message{ + curMessage := service.Message{ Id: messageIdSequence, Content: content, CreateTime: time.Now().Format(time.Kitchen), @@ -38,7 +39,7 @@ func MessageAction(c *gin.Context) { if messages, exist := tempChat[chatKey]; exist { tempChat[chatKey] = append(messages, curMessage) } else { - tempChat[chatKey] = []Message{curMessage} + tempChat[chatKey] = []service.Message{curMessage} } c.JSON(http.StatusOK, Response{StatusCode: 0}) } else { diff --git a/Go-Project/controller/publish.go b/Go-Project/controller/publish.go new file mode 100644 index 00000000..15a285cb --- /dev/null +++ b/Go-Project/controller/publish.go @@ -0,0 +1,112 @@ +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/service" + "io/ioutil" + "net/http" + "path/filepath" + "strconv" + "time" +) + +type VideoListResponse struct { + Response + VideoList []Video `json:"video_list"` +} + +// Publish check token then save upload file to public directory and mysql +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_%d_%s", user.Id, time.Now().Unix(), filename) + saveFilePath := filepath.Join("./public/", finalName) + if err := c.SaveUploadedFile(data, saveFilePath); err != nil { + c.JSON(http.StatusOK, Response{ + StatusCode: 1, + StatusMsg: err.Error(), + }) + return + } + newId := len(DemoVideos) + 1 + resp, err := http.Get("https://api.ipify.org?format=text") + if err != nil { + return + } + defer resp.Body.Close() + + ip, err := ioutil.ReadAll(resp.Body) + if err != nil { + return + } + //内网测试ip + //ip := "10.200.138.3" + playUrl := "http://" + string(ip) + ":8080" + "/static/" + finalName + DemoVideos = append(DemoVideos, Video{ + Id: int64(newId), + Author: user, + PlayUrl: playUrl, + CoverUrl: "", + FavoriteCount: 0, + CommentCount: 0, + IsFavorite: false, + }) + title := c.PostForm("title") + err = service.SaveVideo(service.Video{ + ID: int64(newId), + AuthorID: user.Id, + PlayURL: playUrl, + CoverURL: "", + Title: title, + }) + if err != nil { + c.JSON(http.StatusOK, Response{ + StatusCode: 1, + StatusMsg: err.Error(), + }) + return + } + c.JSON(http.StatusOK, Response{ + StatusCode: 0, + StatusMsg: finalName + " uploaded successfully", + }) +} + +func PublishList(c *gin.Context) { + strUserId := c.Query("user_id") + userId, err := strconv.ParseInt(strUserId, 10, 64) + if err != nil { + c.JSON(http.StatusBadRequest, VideoListResponse{ + Response: Response{1, err.Error()}, + VideoList: nil, + }) + return + } + var userPublishVideos []Video + for _, video := range DemoVideos { + if video.Author.Id == userId { + userPublishVideos = append(userPublishVideos, video) + } + } + c.JSON(http.StatusOK, VideoListResponse{ + Response: Response{0, "response successfully"}, + VideoList: userPublishVideos, + }) +} diff --git a/controller/relation.go b/Go-Project/controller/relation.go similarity index 91% rename from controller/relation.go rename to Go-Project/controller/relation.go index 1eea2816..1ba8a8b9 100644 --- a/controller/relation.go +++ b/Go-Project/controller/relation.go @@ -27,7 +27,7 @@ func FollowList(c *gin.Context) { Response: Response{ StatusCode: 0, }, - UserList: []User{DemoUser}, + //UserList: []User{DemoUser}, }) } @@ -37,7 +37,7 @@ func FollowerList(c *gin.Context) { Response: Response{ StatusCode: 0, }, - UserList: []User{DemoUser}, + //UserList: []User{DemoUser}, }) } @@ -47,6 +47,6 @@ func FriendList(c *gin.Context) { Response: Response{ StatusCode: 0, }, - UserList: []User{DemoUser}, + //UserList: []User{DemoUser}, }) } diff --git a/Go-Project/controller/user.go b/Go-Project/controller/user.go new file mode 100644 index 00000000..29dc8a47 --- /dev/null +++ b/Go-Project/controller/user.go @@ -0,0 +1,163 @@ +package controller + +import ( + "log" + "net/http" + "strconv" + "sync/atomic" + + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/dao" + + "github.com/life-studied/douyin-simple/service" +) + +// 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{} + +var userIdSequence = int64(0) + +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") + var enToken string + + //合法性校验 + err := service.IsUserLegal(username, password) + if err != nil { + c.JSON(http.StatusBadRequest, UserLoginResponse{ + Response: Response{StatusCode: 1, StatusMsg: err.Error()}, + }) + return + } + + //获取数据库所有数据 + users, err := service.RequireAllUser() + if err != nil { + c.JSON(http.StatusInternalServerError, UserLoginResponse{ + Response: Response{StatusCode: 1, StatusMsg: "获取数据失败"}, + }) + return + } + // 遍历查询结果,存入映射 + var idmax int64 = 0 + for _, user := range users { + idmax = user.ID + enToken = service.Encryption(user.Name, user.Password) + Info := User{ + Id: user.ID, + Name: user.Name, + } + usersLoginInfo[enToken] = Info + } + userIdSequence = idmax + + //判断用户是否重复 + flag := service.IsUsernameExists(username) + if flag { + enToken = service.Encryption(username, password) //生成token并进行加密 + } else { + c.JSON(http.StatusConflict, UserLoginResponse{ + Response: Response{StatusCode: 1, StatusMsg: "该用户名已存在"}, + }) + return + } + + //将id加一后注册用户存入映射中 + atomic.AddInt64(&userIdSequence, 1) + newUser := User{ + Id: userIdSequence, + Name: username, + } + usersLoginInfo[enToken] = newUser + + //存入数据库 + err = service.CreateInfo(userIdSequence, username, password) + if err != nil { + c.JSON(http.StatusInternalServerError, UserListResponse{ + Response: Response{StatusCode: 1, StatusMsg: "存储用户信息失败"}, + }) + return + } + + //返回正确响应 + c.JSON(http.StatusOK, UserLoginResponse{ + Response: Response{StatusCode: 0, StatusMsg: "注册成功"}, + UserId: userIdSequence, + Token: enToken, + }) + +} + +func Login(c *gin.Context) { + username := c.Query("username") + password := c.Query("password") + var enToken string + + token := username + password + enToken = service.Encryption(username, password) + + _, err := service.LoginUser(username, password) + if err != nil { + c.JSON(http.StatusOK, UserLoginResponse{ + Response: Response{StatusCode: 1, StatusMsg: "登录失败!请检查用户名和密码。"}, + }) + return + } + + //在映射中用token查id + loggedInUser, found := usersLoginInfo[enToken] + if !found { + c.JSON(http.StatusInternalServerError, UserLoginResponse{ + Response: Response{StatusCode: 1, StatusMsg: "登录失败!用户信息不存在。"}, + }) + return + } + + //返回正确响应 + c.JSON(http.StatusOK, UserLoginResponse{ + Response: Response{StatusCode: 0, StatusMsg: "登录成功!"}, + UserId: loggedInUser.Id, + Token: token, + }) +} + +func UserInfo(c *gin.Context) { + userId := c.Query("user_id") + id, _ := strconv.ParseInt(userId, 10, 64) + token := c.Query("token") + + log.Printf("id = %v, token = %v", id, token) + + if user, exist := dao.GetUserByUserId(id); exist != nil { + + c.JSON(http.StatusOK, UserResponse{ + Response: Response{StatusCode: 1, StatusMsg: "User doesn't exist"}, + }) + } else { + token := user.Name + user.Password + respUser, exists := usersLoginInfo[token] + if !exists { + return + } + service.MapToJson(respUser) + c.JSON(http.StatusOK, UserResponse{ + Response: Response{StatusCode: 0}, + User: respUser, + }) + } +} diff --git a/Go-Project/dao/comment.go b/Go-Project/dao/comment.go new file mode 100644 index 00000000..23b423a7 --- /dev/null +++ b/Go-Project/dao/comment.go @@ -0,0 +1,66 @@ +package dao + +import ( + "errors" + "fmt" + "github.com/life-studied/douyin-simple/global" + "github.com/life-studied/douyin-simple/model" + "gorm.io/gorm" +) + +// QueryCommentsByVideoId 根据视频id查询该视频的评论列表 +func QueryCommentsByVideoId(videoId int64) ([]model.Comment, error) { + var comments []model.Comment + if err := global.DB.Preload("User").Where("video_id = ?", videoId).Find(&comments).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fmt.Errorf("没有找到%d这个视频的评论!", videoId) + } + return nil, fmt.Errorf("查询评论失败:%w", err) + } + return comments, nil +} + +// GetUserById 根据user_id返回用户结构体 +func GetUserById(userID int64) (model.User, error) { + var user model.User + err := global.DB.Where("id = ?", userID).First(&user).Error + if err != nil { + return model.User{}, err + } + return user, nil +} + +// GetCommentById 通过commentID 返回comment结构体 +func GetCommentById(commentID int64) (model.Comment, error) { + var comment model.Comment + err := global.DB.Where("id = ?", commentID).First(&comment).Error + if err != nil { + return model.Comment{}, err + } + return comment, nil +} + +// CreateComment 创建评论 +func CreateComment(comment *model.Comment) error { + var maxID int + global.DB.Table("comments").Select("MAX(id)").Scan(&maxID) + comment.Id = int64(maxID + 1) + err := global.DB.Create(&comment).Error + return err +} + +// DeleteCommentById 根据id删除评论 +func DeleteCommentById(commentID int64) error { + err := global.DB.Where("id = ?", commentID).Delete(model.Comment{}).Error + return err +} + +// InCreCommentCount 增加评论数量 +func InCreCommentCount(videoId int64, count int) error { + return global.DB.Model(&model.Video{}).Where("id = ?", videoId).Update("comment_count", gorm.Expr("comment_count + ?", count)).Error +} + +// DeCreCommentCount 减少评论数量 +func DeCreCommentCount(videoId int64, count int) error { + return global.DB.Model(&model.Video{}).Where("id = ?", videoId).Update("comment_count", gorm.Expr("comment_count - ?", count)).Error +} diff --git a/Go-Project/dao/common.go b/Go-Project/dao/common.go new file mode 100644 index 00000000..90ba725b --- /dev/null +++ b/Go-Project/dao/common.go @@ -0,0 +1,69 @@ +// Package dao ----------------------------- +// @file : common.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/19 19:45 +// ------------------------------------------- +package dao + +import ( + "github.com/life-studied/douyin-simple/global" + "gorm.io/gorm" + "time" +) + +type User struct { + ID int64 `gorm:"primary_key;column:id;comment:'用户ID'" json:"id"` + Name string `gorm:"column:name;not null;comment:'用户名'" json:"name"` + Password string `gorm:"column:password;not null;comment:'密码'" json:"password"` +} + +type Video struct { + ID int64 `gorm:"primary_key;column:id;comment:'视频ID'" json:"id"` + AuthorID int64 `gorm:"column:author_id;not null;comment:'作者ID'" json:"author_id"` + PlayURL string `gorm:"column:play_url;not null;comment:'播放链接'" json:"play_url"` + CoverURL string `gorm:"column:cover_url;not null;comment:'封面链接'" json:"cover_url"` + Title string `gorm:"column:title;not null;comment:'标题'" json:"title"` + PublishTime time.Time `gorm:"column:publish_time;not null;comment:'发布时间戳'" json:"publish_time"` +} + +type Follow struct { + UserID int64 `gorm:"column:user_id;not null;comment:'用户ID'" json:"user_id"` + FollowUserID int64 `gorm:"column:follow_user_id;not null;comment:'被关注的用户ID'" json:"follow_user_id"` + + // Foreign key references + User User `gorm:"foreignkey:UserID" json:"-"` + FollowUser User `gorm:"foreignkey:FollowUserID" json:"-"` +} + +type Comment struct { + ID int64 `gorm:"primary_key;column:id;comment:'评论ID'" json:"id"` + UserID int64 `gorm:"column:user_id;not null;comment:'用户ID'" json:"user_id"` + VideoID int64 `gorm:"column:video_id;not null;comment:'视频ID'" json:"video_id"` + Content string `gorm:"column:content;not null;comment:'评论内容'" json:"content"` + CreateDate int64 `gorm:"column:create_date;not null;comment:'创建日期'" json:"create_date"` + + // Foreign key references + User User `json:"user" gorm:"foreignKey:user_id;references:id;"` + Video Video `gorm:"foreignkey:VideoID" json:"-"` +} + +type Like struct { + ID int64 `gorm:"column:id;not null;comment:'主键ID'" json:"id"` + UserID int64 `gorm:"column:user_id;not null;comment:'点赞者ID'" json:"user_id"` + VideoID int64 `gorm:"column:video_id;not null;comment:'视频ID'" json:"video_id"` + + // Foreign key references + User User `gorm:"foreignkey:UserID" json:"user"` + Video Video `gorm:"foreignkey:VideoID" json:"-"` +} + +// BeginTransaction 开始事务 +func BeginTransaction() *gorm.DB { + return global.DB.Begin() +} + +// RollbackTransaction 回滚事务 +func RollbackTransaction(tx *gorm.DB) { + tx.Rollback() +} diff --git a/Go-Project/dao/favorite.go b/Go-Project/dao/favorite.go new file mode 100644 index 00000000..7b62d9cd --- /dev/null +++ b/Go-Project/dao/favorite.go @@ -0,0 +1,61 @@ +package dao + +import ( + "github.com/life-studied/douyin-simple/global" + "time" +) + +func InsertFavoriteVideo(user User, video Video) (err error) { + var likeok []Like + // 判断用户是否已经收藏过该视频 + global.DB.Where("user_id=? and video_id=?", user.ID, video.ID).Find(&likeok).First(&likeok) + if len(likeok) > 0 { + return nil + } + var saveUser []User + var saveVideo []Video + err = global.DB.Where("id=?", user.ID).Find(&saveUser).Error + if err != nil { + return err + } + err = global.DB.Where("id=?", video.ID).Find(&saveVideo).Error + if err != nil { + return err + } + like := &Like{ + ID: time.Now().Unix(), + UserID: user.ID, + VideoID: video.ID, + User: saveUser[0], + Video: saveVideo[0], + } + return global.DB.Create(like).Error + +} + +func DeleteFavoriteVideo(user User, video Video) (err error) { + var like Like + err = global.DB.Where("user_id = ? and video_id = ?", user.ID, video.ID).Find(&like).Error + if err != nil { + return err + } + return global.DB.Delete(like).Error +} + +func GetFavoriteVideo(user User) (likes []Like, err error) { + err = global.DB.Where("user_id = ?", user.ID).Find(&likes).Error + for i := range likes { + global.DB.Where("id = ?", likes[i].UserID).Find(&likes[i].User) + global.DB.Where("id = ?", likes[i].VideoID).Find(&likes[i].Video) + } + return likes, err +} + +func GetFavoriteUser(video Video) (likes []Like, err error) { + err = global.DB.Where("video_id = ?", video.ID).Find(&likes).Error + for i := range likes { + global.DB.Where("id = ?", likes[i].UserID).Find(&likes[i].User) + global.DB.Where("id = ?", likes[i].VideoID).Find(&likes[i].Video) + } + return likes, err +} diff --git a/Go-Project/dao/feed.go b/Go-Project/dao/feed.go new file mode 100644 index 00000000..92be66aa --- /dev/null +++ b/Go-Project/dao/feed.go @@ -0,0 +1,34 @@ +// Package dao ----------------------------- +// @file : feed.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/22 23:47 +// ------------------------------------------- +package dao + +import ( + "github.com/life-studied/douyin-simple/global" + "time" +) + +func QueryNextTimeByLatestTime(latestTime time.Time) (int64, int64, error) { + var videos []Video + err := global.DB.Where("publish_time <= ?", latestTime).Order("publish_time desc").Limit(30).Find(&videos).Error + if err != nil { + return 0, 0, err + } + var nextTime int64 + var ID int64 + //if len(videos) < 30 { + // err = global.DB.Where("publish_time <= ?", latestTime).Order("publish_time asc").First(&videos[0]).Error + // if err != nil { + // return 0, 0, err + // } + // nextTime = videos[0].PublishTime.Unix() + // ID = videos[0].ID + //} else { + // nextTime = videos[len(videos)-1].PublishTime.Unix() + // ID = videos[len(videos)-1].ID + //} + return nextTime, ID, nil +} diff --git a/Go-Project/dao/init_cache.go b/Go-Project/dao/init_cache.go new file mode 100644 index 00000000..d59a8fcc --- /dev/null +++ b/Go-Project/dao/init_cache.go @@ -0,0 +1,41 @@ +// Package dao ----------------------------- +// @file : init_cache.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/29 0:06 +// ------------------------------------------- +package dao + +import ( + "github.com/life-studied/douyin-simple/global" +) + +func InitVideoDataFromMysql() ([]Video, error) { + var videos []Video + err := global.DB.Select("*").Find(&videos).Error + return videos, err +} + +func InitUserByUserIdFromMysql(userId int64) (string, string, error) { + var user User + err := global.DB.Where("id=?", userId).Find(&user).Error + return user.Name, user.Password, err +} + +func InitUserFromMysql() ([]User, error) { + var users []User + err := global.DB.Select("*").Find(&users).Error + return users, err +} + +func InitVideoFavoriteCountFromMysql(videoId int64) (int64, error) { + var count int64 + err := global.DB.Model(&Like{}).Where("video_id=?", videoId).Count(&count).Error + return count, err +} + +func InitVideoCommentCountFromMysql(videoId int64) (int64, error) { + var count int64 + err := global.DB.Model(&Comment{}).Where("video_id=?", videoId).Count(&count).Error + return count, err +} diff --git a/Go-Project/dao/publish.go b/Go-Project/dao/publish.go new file mode 100644 index 00000000..740bb356 --- /dev/null +++ b/Go-Project/dao/publish.go @@ -0,0 +1,19 @@ +// Package dao ----------------------------- +// @file : publish.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/24 17:21 +// ------------------------------------------- +package dao + +import ( + "github.com/life-studied/douyin-simple/global" +) + +func SaveVideoToMysql(newVideo Video) error { + err := global.DB.Create(&newVideo).Error + if err != nil { + return err + } + return nil +} diff --git a/Go-Project/dao/user.go b/Go-Project/dao/user.go new file mode 100644 index 00000000..f1348689 --- /dev/null +++ b/Go-Project/dao/user.go @@ -0,0 +1,79 @@ +package dao + +import ( + "errors" + "github.com/life-studied/douyin-simple/model" + + "github.com/life-studied/douyin-simple/global" + "gorm.io/gorm" +) + +// 查询用户名是否存在 +func QueryName(username string) bool { + //使用gorm查询用户名是否存在 + var users []User + result := global.DB.Where("name=?", username).First(&users) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + if result.RowsAffected == 0 { + return true + } + } else { + panic(result.Error) + } + + } + return false +} + +// 将用户信息存入数据库 +func AddUserInfo(id int64, username string, password string) error { + + //将id username password 存入数据库中 + user := User{ + ID: id, + Name: username, + Password: password, + } + tResult := global.DB.Create(&user) + if tResult.Error != nil { + return tResult.Error + } + + return nil +} + +// 获取所有数据 +func GetAllUsers() ([]User, error) { + //数据库连接 + var users []User + result := global.DB.Find(&users) + if result.Error != nil { + return nil, result.Error + } + + return users, nil + +} + +// 查询用户名和密码 +func GetUserByUsernameAndPassword(username, password string) (User, error) { + var users []User + result := global.DB.Where("name = ? AND password = ?", username, password).First(&users) + if result.Error != nil { + if errors.Is(result.Error, gorm.ErrRecordNotFound) { + return User{}, errors.New("User not found") + } + return User{}, result.Error + } + return User{}, nil +} + +func QueryUserById(id int64) (*model.User, error) { + var user model.User + err := global.DB.Where("id = ?", id).First(&user).Error + if err != nil { + return nil, err + } + return &user, nil +} diff --git a/Go-Project/dao/userdao.go b/Go-Project/dao/userdao.go new file mode 100644 index 00000000..c5ab14da --- /dev/null +++ b/Go-Project/dao/userdao.go @@ -0,0 +1,16 @@ +package dao + +import ( + "log" + + "github.com/life-studied/douyin-simple/global" +) + +func GetUserByUserId(id int64) (User, error) { + user := User{} + if err := global.DB.Where("id = ?", id).First(&user).Error; err != nil { + log.Println(err.Error()) + return user, err + } + return user, nil +} diff --git a/Go-Project/global/global.go b/Go-Project/global/global.go new file mode 100644 index 00000000..a1f5f181 --- /dev/null +++ b/Go-Project/global/global.go @@ -0,0 +1,11 @@ +package global + +import ( + "github.com/life-studied/douyin-simple/config" + "gorm.io/gorm" +) + +var ( + DB *gorm.DB + CONFIG config.Server +) diff --git a/Go-Project/go.mod b/Go-Project/go.mod new file mode 100644 index 00000000..1516544c --- /dev/null +++ b/Go-Project/go.mod @@ -0,0 +1,12 @@ +module github.com/life-studied/douyin-simple + +go 1.16 + +require ( + github.com/gavv/httpexpect/v2 v2.8.0 + github.com/gin-gonic/gin v1.9.1 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.3 + gorm.io/driver/mysql v1.5.1 + gorm.io/gorm v1.25.4 +) diff --git a/Go-Project/initialize/config.go b/Go-Project/initialize/config.go new file mode 100644 index 00000000..ab27a60a --- /dev/null +++ b/Go-Project/initialize/config.go @@ -0,0 +1,33 @@ +package initialize + +import ( + "fmt" + "github.com/life-studied/douyin-simple/global" + "github.com/spf13/viper" + "os" +) + +// Read ReadConfig +func Read(configName string, configPath string, configType string) *viper.Viper { + v := viper.New() + v.SetConfigName(configName) + v.AddConfigPath(configPath) + v.SetConfigType(configType) + err := v.ReadInConfig() + if err != nil { + panic(fmt.Errorf("Fatal error config file: %s \n", err)) + } + return v +} + +// Config InitConfig +func Config() { + path, err := os.Getwd() //返回项目目录 + if err != nil { + panic(err) + } + v := Read("config", path, "yaml") //在项目目录底下查找config.yaml配置文件 + if err := v.Unmarshal(&global.CONFIG); err != nil { + panic(err) + } +} diff --git a/Go-Project/initialize/mysql.go b/Go-Project/initialize/mysql.go new file mode 100644 index 00000000..fbe0f311 --- /dev/null +++ b/Go-Project/initialize/mysql.go @@ -0,0 +1,31 @@ +package initialize + +import ( + "github.com/life-studied/douyin-simple/global" + "gorm.io/driver/mysql" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +func Mysql() { + + mysqlConfig := mysql.Config{ + DSN: global.CONFIG.Mysql.Dsn(), // DSN data source name + SkipInitializeWithVersion: false, // 根据版本自动配置 + } + var err error + global.DB, err = gorm.Open(mysql.New(mysqlConfig), + &gorm.Config{ + PrepareStmt: true, + SkipDefaultTransaction: true, + Logger: logger.Default.LogMode(logger.Info), + }, + ) //启动MySQL,启用预编译SQL语句,不会自动开启事务等功能 + if err != nil { + panic(err) + } else { + sqlDB, _ := global.DB.DB() + sqlDB.SetMaxIdleConns(global.CONFIG.Mysql.MaxIdleConns) // 设置空闲连接池中连接的最大数量 + sqlDB.SetMaxOpenConns(global.CONFIG.Mysql.MaxOpenConns) // 设置打开数据库连接的最大数量 + } +} diff --git a/Go-Project/main.go b/Go-Project/main.go new file mode 100644 index 00000000..71d6fbf1 --- /dev/null +++ b/Go-Project/main.go @@ -0,0 +1,21 @@ +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/controller" + "github.com/life-studied/douyin-simple/initialize" + "github.com/life-studied/douyin-simple/service" +) + +func main() { + go service.RunMessageServer() + + r := gin.Default() + initialize.Config() + initialize.Mysql() + controller.InitCacheFromMysql() + initRouter(r) + + r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") + +} diff --git a/Go-Project/model/comment.go b/Go-Project/model/comment.go new file mode 100644 index 00000000..f7c61173 --- /dev/null +++ b/Go-Project/model/comment.go @@ -0,0 +1,21 @@ +package model + +type Comment struct { + Id int64 `json:"id,omitempty" gorm:"primaryKey;autoIncrement:true"` + UserId int64 `json:"-"` + VideoId int64 `json:"-"` + User User `json:"user,omitempty" gorm:"foreignKey:user_id;references:id;"` + Content string `json:"content,omitempty"` + CreateDate string `json:"create_date,omitempty"` + ID uint +} + +type Video struct { + Video_id int64 `gorm:"column:id;AUTO_INCREMENT;PRIMARY_KEY;not null" json:"video_id"` + Author_id int64 `gorm:"column:author_id;type:int;not null" json:"author_id"` + Play_url string `gorm:"column:play_url;type:varchar(255);not null" json:"play_url"` + Cover_url string `gorm:"column:name;type:varchar(255);not null" json:"cover_url"` + Comment_count int64 `gorm:"column:comment_count;type:int;default:0" json:"comment_count"` + Title string `gorm:"column:title;type:varchar(255);not null" json:"title"` + Publish_time int64 `gorm:"column:publish_time;type:bigint;not null" json:"publish_time"` +} diff --git a/Go-Project/model/user.go b/Go-Project/model/user.go new file mode 100644 index 00000000..5478c0f4 --- /dev/null +++ b/Go-Project/model/user.go @@ -0,0 +1,7 @@ +package model + +type User struct { + Id int64 `json:"id,omitempty" gorm:"primaryKey"` + Name string `json:"name,omitempty"` + Password string `json:"-" ` +} diff --git a/Go-Project/response/comment.go b/Go-Project/response/comment.go new file mode 100644 index 00000000..9aeb2d2a --- /dev/null +++ b/Go-Project/response/comment.go @@ -0,0 +1,32 @@ +package response + +import "github.com/life-studied/douyin-simple/model" + +// User_Response 用户信息的响应结构体 +type User_Response struct { + ID uint `json:"id,omitempty"` + Name string `json:"name,omitempty"` +} + +// Comment_Response 评论信息的响应结构体 +type Comment_Response struct { + ID uint `json:"id,omitempty"` + Content string `json:"content,omitempty"` + CreateDate string `json:"create_date,omitempty"` + Userresponse User_Response `json:"user,omitempty"` +} + +type Response struct { + StatusCode int32 `json:"status_code"` + StatusMsg string `json:"status_msg,omitempty"` +} + +type CommentListResponse struct { + Response + CommentList []model.Comment `json:"comment_list,omitempty"` +} + +type CommentActionResponse struct { + Response + Comment model.Comment `json:"comment,omitempty"` +} diff --git a/router.go b/Go-Project/router.go similarity index 95% rename from router.go rename to Go-Project/router.go index 8183f38c..5a56e884 100644 --- a/router.go +++ b/Go-Project/router.go @@ -1,8 +1,8 @@ package main import ( - "github.com/RaymondCode/simple-demo/controller" "github.com/gin-gonic/gin" + "github.com/life-studied/douyin-simple/controller" ) func initRouter(r *gin.Engine) { diff --git a/Go-Project/service/CommentService.go b/Go-Project/service/CommentService.go new file mode 100644 index 00000000..d7c1fdfd --- /dev/null +++ b/Go-Project/service/CommentService.go @@ -0,0 +1,171 @@ +package service + +import ( + "fmt" + "github.com/life-studied/douyin-simple/dao" + "github.com/life-studied/douyin-simple/model" + "github.com/life-studied/douyin-simple/response" + "net/http" + "time" +) + +func IntTime2CommentTime(intTime int64) string { + + template := "01-02" + return time.Unix(intTime, 0).Format(template) +} + +func IntTime2StrTime(intTime int64) string { + template := "2006-01-02 15:04:05" + return time.Unix(intTime, 0).Format(template) +} + +func CreateComment(userID int64, videoID int64, commentText string) (response.CommentActionResponse, error) { + // 获取评论时间 + currentTime := time.Now().Unix() + user, err := dao.QueryUserById(userID) + if err != nil { + // 处理videoID解析错误 + commentActionResponse := response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "用户查询异常"}, + } + return commentActionResponse, err + } + + // 发布评论 + // 创建comment结构体 + comment := model.Comment{ + UserId: userID, + VideoId: videoID, + User: *user, + Content: commentText, + CreateDate: IntTime2StrTime(currentTime), + } + // 将comment增添到数据库中 + tx := dao.BeginTransaction() + err = dao.CreateComment(&comment) + if err != nil { + // 如果发生错误,将数据库回滚到未添加评论的初始状态 + defer dao.RollbackTransaction(tx) + fmt.Printf("添加评论异常:%s", err) + + commentActionResponse := response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "添加评论异常"}, + } + return commentActionResponse, err + + } + // 更新视频表评论总数+1 + //err = dao.InCreCommentCount(videoID, 1) + //if err != nil { + // // 如果发生错误,将数据库回滚到未添加评论的初始状态 + // defer dao.RollbackTransaction(tx) + // fmt.Printf("更新评论总数异常:%s", err) + // + // commentActionResponse := response.CommentActionResponse{ + // Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "更新视频评论数异常"}, + // } + // return commentActionResponse, err + // + //} + + // 创建Comment_Response响应结构体 + createDate := IntTime2CommentTime(currentTime) + commenter, err := dao.GetUserById(userID) + if err != nil { + // 如果发生错误,将数据库回滚到未添加评论的初始状态 + defer dao.RollbackTransaction(tx) + fmt.Printf("获取用户异常:%s", err) + + commentActionResponse := response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "添加评论异常"}, + } + return commentActionResponse, err + } + // 返回响应 + commentActionResponse := response.CommentActionResponse{ + Response: response.Response{StatusCode: 0, StatusMsg: "OK"}, + Comment: model.Comment{ + ID: comment.ID, + Content: comment.Content, + CreateDate: createDate, + User: model.User{ + Id: commenter.Id, + Name: commenter.Name, + }, + }, + } + return commentActionResponse, err +} + +func DeleteComment(userID int64, videoID int64, commentID int64) (response.CommentActionResponse, error) { + var commentActionResponse response.CommentActionResponse + // 删除评论 + // 根据commentID在数据库中找到待删除的评论 + + // 判断是否有权限删除 + // 通过commentID找到commenterID + comment, err := dao.GetCommentById(commentID) + if err != nil { + fmt.Printf("获取评论异常:%s", err) + + commentActionResponse = response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "获取评论异常"}, + } + return commentActionResponse, err + } + commenterID := comment.UserId + // 若有权限,则删除id为commentID评论;若无权限,则拒绝删除 + if commenterID == userID { + tx := dao.BeginTransaction() + err = dao.DeleteCommentById(commentID) + if err != nil { + // 如果发生错误,将数据库回滚到未删除评论的初始状态 + defer dao.RollbackTransaction(tx) + fmt.Printf("删除评论异常:%s", err) + + commentActionResponse = response.CommentActionResponse{ + Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "删除评论异常"}, + } + + return commentActionResponse, err + } + // 更新视频表评论总数-1 + //err = dao.DeCreCommentCount(videoID, -1) + //if err != nil { + // // 如果发生错误,将数据库回滚到未删除评论的初始状态 + // defer dao.RollbackTransaction(tx) + // fmt.Printf("更新视频评论数异常:%s", err) + // + // commentActionResponse = response.CommentActionResponse{ + // Response: response.Response{StatusCode: http.StatusInternalServerError, StatusMsg: "更新视频评论数异常"}, + // } + // + // return commentActionResponse, err + //} + + commentActionResponse = response.CommentActionResponse{ + Response: response.Response{StatusCode: 0, StatusMsg: "删除成功"}, + Comment: model.Comment{}, + } + + } else { + commentActionResponse = response.CommentActionResponse{ + Response: response.Response{StatusCode: 0, StatusMsg: "无删除权限"}, + Comment: model.Comment{}, + } + } + return commentActionResponse, err +} + +func GetCommentList(videoID int64) ([]model.Comment, error) { + + var rawComments []model.Comment + var err error + rawComments, err = dao.QueryCommentsByVideoId(videoID) + if err != nil { + return nil, err + } + return rawComments, nil + +} diff --git a/Go-Project/service/MapToJson.go b/Go-Project/service/MapToJson.go new file mode 100644 index 00000000..f743e3e4 --- /dev/null +++ b/Go-Project/service/MapToJson.go @@ -0,0 +1,10 @@ +package service + +import "encoding/json" + +func MapToJson(Mapstruct interface{}) string { + // map转 json str + jsonBytes, _ := json.Marshal(Mapstruct) + jsonStr := string(jsonBytes) + return jsonStr +} diff --git a/Go-Project/service/favorite.go b/Go-Project/service/favorite.go new file mode 100644 index 00000000..1c44f7e5 --- /dev/null +++ b/Go-Project/service/favorite.go @@ -0,0 +1,73 @@ +package service + +import ( + "fmt" + "github.com/life-studied/douyin-simple/dao" + "strconv" +) + +type FavoriteVideoID struct { + VideoID string + Author FavoriteUserID + PlayUrl string + CoverUrl string + FavoriteCount int64 + CommentCount int64 +} + +type FavoriteUserID struct { + UserName string + UserID int64 + FollowCount int64 + FollowerCount int64 +} + +type FavoriteVideoList struct { + Id int64 + Author FavoriteUserID + PlayUrl string + CoverUrl string + FavoriteCount int64 + CommentCount int64 +} + +func FavoriteVideo(video FavoriteVideoID, user FavoriteUserID) error { + videoID, err := strconv.ParseInt(video.VideoID, 10, 64) + if err != nil { + return err + } + err = dao.InsertFavoriteVideo(dao.User{ID: user.UserID}, dao.Video{ID: videoID}) + return err +} + +func UnfavoriteVideo(video FavoriteVideoID, user FavoriteUserID) error { + videoID, err := strconv.ParseInt(video.VideoID, 10, 64) + if err != nil { + fmt.Println("videoID is not int64") + return err + } + err = dao.DeleteFavoriteVideo(dao.User{ID: user.UserID}, dao.Video{ID: videoID}) + return err +} + +func ReadFavoriteVideo(user FavoriteUserID) (videoList []FavoriteVideoList, err error) { + likes, err := dao.GetFavoriteVideo(dao.User{ID: user.UserID}) + if err != nil { + return nil, err + } + videoList = make([]FavoriteVideoList, 0, len(likes)) + for _, like := range likes { + node := like.Video + author := like.User + videoList = append(videoList, FavoriteVideoList{ + Id: node.ID, + Author: FavoriteUserID{ + UserName: author.Name, + UserID: author.ID, + }, + PlayUrl: node.PlayURL, + CoverUrl: node.CoverURL, + }) + } + return videoList, nil +} diff --git a/Go-Project/service/feed.go b/Go-Project/service/feed.go new file mode 100644 index 00000000..46884967 --- /dev/null +++ b/Go-Project/service/feed.go @@ -0,0 +1,27 @@ +// Package service ----------------------------- +// @file : feed.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/22 23:39 +// ------------------------------------------- +package service + +import ( + "github.com/life-studied/douyin-simple/dao" + "strconv" + "time" +) + +func GetNextTime(latest_time string) (int64, int64, error) { + i64LatestTime, err := strconv.ParseInt(latest_time, 10, 64) + i64LatestTime /= 1000 + if err != nil { + return 0, 0, err + } + tmLatestTime := time.Unix(i64LatestTime, 0) + nextTime, startId, err := dao.QueryNextTimeByLatestTime(tmLatestTime) + if err != nil { + return 0, 0, err + } + return nextTime, startId, nil +} diff --git a/service/message.go b/Go-Project/service/message.go similarity index 70% rename from service/message.go rename to Go-Project/service/message.go index 9ba9cdaf..458f31f7 100644 --- a/service/message.go +++ b/Go-Project/service/message.go @@ -3,12 +3,28 @@ package service import ( "encoding/json" "fmt" - "github.com/RaymondCode/simple-demo/controller" "io" "net" "sync" ) +type Message struct { + Id int64 `json:"id,omitempty"` + Content string `json:"content,omitempty"` + CreateTime string `json:"create_time,omitempty"` +} + +type MessageSendEvent struct { + UserId int64 `json:"user_id,omitempty"` + ToUserId int64 `json:"to_user_id,omitempty"` + MsgContent string `json:"msg_content,omitempty"` +} + +type MessagePushEvent struct { + FromUserId int64 `json:"user_id,omitempty"` + MsgContent string `json:"msg_content,omitempty"` +} + var chatConnMap = sync.Map{} func RunMessageServer() { @@ -43,7 +59,7 @@ func process(conn net.Conn) { continue } - var event = controller.MessageSendEvent{} + var event = MessageSendEvent{} _ = json.Unmarshal(buf[:n], &event) fmt.Printf("Receive Message:%+v\n", event) @@ -60,7 +76,7 @@ func process(conn net.Conn) { continue } - pushEvent := controller.MessagePushEvent{ + pushEvent := MessagePushEvent{ FromUserId: event.UserId, MsgContent: event.MsgContent, } diff --git a/Go-Project/service/publish.go b/Go-Project/service/publish.go new file mode 100644 index 00000000..bfcf0518 --- /dev/null +++ b/Go-Project/service/publish.go @@ -0,0 +1,35 @@ +// Package service ----------------------------- +// @file : publish.go +// @author : Yunyin +// @contact : yunyin_jayyi@qq.com +// @time : 2023/8/24 16:56 +// ------------------------------------------- +package service + +import ( + "github.com/life-studied/douyin-simple/dao" + "time" +) + +type Video struct { + ID int64 `gorm:"primary_key;column:id;comment:'视频ID'" json:"id"` + AuthorID int64 `gorm:"column:author_id;not null;comment:'作者ID'" json:"author_id"` + PlayURL string `gorm:"column:play_url;not null;comment:'播放链接'" json:"play_url"` + CoverURL string `gorm:"column:cover_url;not null;comment:'封面链接'" json:"cover_url"` + Title string `gorm:"column:title;not null;comment:'标题'" json:"title"` +} + +func SaveVideo(video Video) error { + err := dao.SaveVideoToMysql(dao.Video{ + ID: video.ID, + AuthorID: video.AuthorID, + PlayURL: video.PlayURL, + CoverURL: video.CoverURL, + Title: video.Title, + PublishTime: time.Now(), + }) + if err != nil { + return err + } + return nil +} diff --git a/Go-Project/service/user.go b/Go-Project/service/user.go new file mode 100644 index 00000000..2ac7b146 --- /dev/null +++ b/Go-Project/service/user.go @@ -0,0 +1,72 @@ +package service + +import ( + "errors" + "fmt" + + "github.com/life-studied/douyin-simple/dao" +) + +const ( + MaxUsernameLength = 32 //用户名最大长度 + MaxPasswordLength = 32 //密码最大长度 + MinPasswordLength = 6 //密码最小长度 +) + +// 合法性加密 +func IsUserLegal(userName string, passWord string) error { + //1.用户名检验 + if userName == "" { + return errors.New("用户名为空") + } + if len(userName) > MaxUsernameLength { + return errors.New("用户名长度不符合规范") + } + //2.密码检验 + if passWord == "" { + return errors.New("密码为空") + } + if len(passWord) > MaxPasswordLength || len(passWord) < MinPasswordLength { + return errors.New("密码长度不符合规范") + } + return nil +} + +// 对token进行加密 +func Encryption(username, password string) string { + token := username + password + return token +} + +// 查询用户名是否重复 +func IsUsernameExists(username string) bool { + return dao.QueryName(username) +} + +// 创建用户并存入数据库 +func CreateInfo(id int64, username string, password string) error { + err := dao.AddUserInfo(id, username, password) + if err != nil { + return err + } + return nil +} + +// 获取数据库所有数据 +func RequireAllUser() ([]dao.User, error) { + users, err := dao.GetAllUsers() + if err != nil { + fmt.Println("获取用户数据出错:", err) + return nil, err + } + return users, nil +} + +// 查询用户名和密码 +func LoginUser(username, password string) (dao.User, error) { + user, err := dao.GetUserByUsernameAndPassword(username, password) + if err != nil { + return dao.User{}, err + } + return user, nil +} diff --git a/test/base_api_test.go b/Go-Project/test/base_api_test.go similarity index 100% rename from test/base_api_test.go rename to Go-Project/test/base_api_test.go diff --git a/test/common.go b/Go-Project/test/common.go similarity index 100% rename from test/common.go rename to Go-Project/test/common.go diff --git a/test/interact_api_test.go b/Go-Project/test/interact_api_test.go similarity index 100% rename from test/interact_api_test.go rename to Go-Project/test/interact_api_test.go diff --git a/test/social_api_test.go b/Go-Project/test/social_api_test.go similarity index 100% rename from test/social_api_test.go rename to Go-Project/test/social_api_test.go diff --git a/README.md b/README.md index 7aef8dd7..223cf342 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,111 @@ -# simple-demo +# douyin-simple -## 抖音项目服务端简单示例 +## 项目简介 -具体功能内容参考飞书说明文档 +`douyin-simple` 是一个模仿抖音的后端学习项目,使用 Go 语言和 HTTP 框架等开发。它旨在帮助开发人员深入学习和理解后端开发的实践,并提供模仿抖音的功能和特点。 -工程无其他依赖,直接编译运行即可 +项目特点: -```shell -go build && ./simple-demo +1. **视频上传与存储**:douyin-simple 实现了视频的上传和存储功能,用户可以通过接口将视频文件上传至服务器,并安全地存储到指定位置。这为开发者提供了探索和实践大型文件处理的机会。 +2. **用户管理与鉴权**:项目中包含用户管理和身份验证的功能。用户可以注册、登录和注销账户,并进行权限管理。通过合理的鉴权机制,保证了用户信息和视频内容的安全性。 +3. **视频列表与推荐算法**:douyin-simple 提供了展示视频列表和推荐算法的功能。用户可以浏览最新发布的视频、热门视频以及个性化推荐的视频内容。这一特点使得项目更加贴近实际产品,并提供了学习和实现推荐算法的机会。 +4. **点赞与评论系统**:用户可以对喜欢的视频进行点赞,并留下评论。点赞和评论系统的实现使得用户之间可以进行互动和交流。这一功能是社交类应用中常见且重要的部分,为开发者提供了实现用户间互动的接口设计和逻辑处理的机会。 +5. **统计与数据分析**:douyin-simple 支持对视频观看量、点赞数等数据进行统计和分析。这一功能使得开发者能够探索使用数据来优化用户体验和提高应用性能的方法。 + +## 启动项目 + +``` +cd ./Go-Project +go mod tidy +go build +./douyin-simple ``` -### 功能说明 +## 项目结构说明 + +> `douyin-simple` +> +> >`.gitignore` -- 忽略提交配置 +> > +> >`README.md` -- 项目自述文件 +> > +> >`documents` -- 环境搭建、编码规范、项目需求等等文档资源 +> > +> >`Go-Project` -- Go后端项目主体文件 + +## 软件架构 + +### Go技术栈 + +#### 后端核心技术栈 + +| 技术 | 说明 | 版本 | 备注 | +| ---------- | ----------------- | ------ | ------------------------------------- | +| `gorm` | `MySql`连接驱动库 | 1.25.4 | https://gorm.io/zh_CN/docs/index.html | +| `go-redis` | `redis`连接客户端 | | https://github.com/redis/go-redis | +| gin | http框架 | 1.9.1 | https://gin-gonic.com/zh-cn/docs/ | + +#### 后端拓展技术栈 + +| 技术 | 说明 | 版本 | 备注 | +| ----- | -------------- | ------ | ------------------------------ | +| viper | 配置解决方案库 | 1.16.0 | https://github.com/spf13/viper | + +## 环境要求 + +### 开发工具 + +| 工具 | 说明 | 版本 | 备注 | +| --------- | --------------------- | ------ | ------------------------------------------------------------ | +| `GoLand` | `Go`开发IDE | | | +| `Git` | 项目版本管控工具 | latest | https://git-scm.com/ | +| `RDM` | `Redis`可视化管理工具 | latest | https://github.com/uglide/RedisDesktopManager https://gitee.com/qishibo/AnotherRedisDesktopManager | +| `Navicat` | 数据库连接工具 | latest | https://www.navicat.com.cn/ | +| `Apipost` | `API`接口调试工具 | latest | https://www.apipost.cn/ | + +### 开发环境 + +| 依赖环境 | 版本 | 备注 | +| --------- | ---- | ------------------------- | +| `Windows` | 10+ | 操作系统 | +| `golang` | 1.20 | https://golang.google.cn/ | + +### 服务器环境 + +| 依赖环境 | 版本 | 备注 | +| -------- | ------ | ------------------------- | +| `CentOS` | 8 | | +| `Docker` | latest | https://www.docker.com/ | +| `MySQL` | 8.0.20 | https://www.mysql.com/cn/ | +| `Redis` | 6.2.7 | https://redis.io/ | + +## 项目搭建 + +​ 详见[项目搭建文档](./documents/01-项目需求/业务参考系统/README.md)。 + +## 运行效果截图 + +### 前端App + +image-20230907092903070 + +image-20230907092945095 -接口功能不完善,仅作为示例 +image-20230907093014712 -* 用户登录数据保存在内存中,单次运行过程中有效 -* 视频上传后会保存到本地 public 目录中,访问时用 127.0.0.1:8080/static/video_name 即可 +### 后端服务器 -### 测试 +![image-20230907092728533](./assets/image-20230907092728533.png) -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 编辑器工具支持页面中找到更为详细的信息: + + +## 指导原则 + +### 指向 interface 的指针 + +您几乎不需要指向接口类型的指针。您应该将接口作为值进行传递,在这样的传递过程中,实质上传递的底层数据仍然可以是指针。 + +接口实质上在底层用两个字段表示: + +1. 一个指向某些特定类型信息的指针。您可以将其视为"type"。 +2. 数据指针。如果存储的数据是指针,则直接存储。如果存储的数据是一个值,则存储指向该值的指针。 + +如果希望接口方法修改基础数据,则必须使用指针传递 (将对象指针赋值给接口变量)。 + +```go +type F interface { + f() +} + +type S1 struct{} + +func (s S1) f() {} + +type S2 struct{} + +func (s *S2) f() {} + +// f1.f() 无法修改底层数据 +// f2.f() 可以修改底层数据,给接口变量 f2 赋值时使用的是对象指针 +var f1 F = S1{} +var f2 F = &S2{} +``` +永远不要使用指向interface的指针,这个是没有意义的.在go语言中,接口本身就是引用类型,换句话说,接口类型本身就是一个指针。对于我的需求,其实test的参数只要是myinterface就可以了,只需要在传值的时候,传*mystruct类型(也只能传*mystruct类型) +``` +type myinterface interface{ + print() +} +func test(value *myinterface){ + //someting to do ... +} + +type mystruct struct { + i int +} +//实现接口 +func (this *mystruct) print(){ + fmt.Println(this.i) + this.i=1 +} +func main(){ +m := &mystruct{0} +test(m)//错误 +test(*m)//错误 +} +``` + +### Interface 合理性验证 + +在编译时验证接口的符合性。这包括: + +- 将实现特定接口的导出类型作为接口 API 的一部分进行检查 +- 实现同一接口的 (导出和非导出) 类型属于实现类型的集合 +- 任何违反接口合理性检查的场景,都会终止编译,并通知给用户 + +补充:上面 3 条是编译器对接口的检查机制, +大体意思是错误使用接口会在编译期报错。 +所以可以利用这个机制让部分问题在编译期暴露。 + + + + + +
BadGood
+ +```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, +) { + // ... +} +``` + +
+ +如果 `*Handler` 与 `http.Handler` 的接口不匹配, +那么语句 `var _ http.Handler = (*Handler)(nil)` 将无法编译通过。 + +赋值的右边应该是断言类型的零值。 +对于指针类型(如 `*Handler`)、切片和映射,这是 `nil`; +对于结构类型,这是空结构。 + +```go +type LogHandler struct { + h http.Handler + log *zap.Logger +} +var _ http.Handler = LogHandler{} +func (h LogHandler) ServeHTTP( + w http.ResponseWriter, + r *http.Request, +) { + // ... +} +``` + +### 接收器 (receiver) 与接口 + +使用值接收器的方法既可以通过值调用,也可以通过指针调用。 + +带指针接收器的方法只能通过指针或 [addressable values] 调用。 + +[addressable values]: https://golang.org/ref/spec#Method_values + +例如, + +```go +type S struct { + data string +} + +func (s S) Read() string { + return s.data +} + +func (s *S) Write(str string) { + s.data = str +} + +sVals := map[int]S{1: {"A"}} + +// 你通过值只能调用 Read +sVals[1].Read() + +// 这不能编译通过: +// sVals[1].Write("test") + +sPtrs := map[int]*S{1: {"A"}} + +// 通过指针既可以调用 Read,也可以调用 Write 方法 +sPtrs[1].Read() +sPtrs[1].Write("test") +``` + +类似的,即使方法有了值接收器,也同样可以用指针接收器来满足接口。 + +```go +type F interface { + f() +} + +type S1 struct{} + +func (s S1) f() {} + +type S2 struct{} + +func (s *S2) f() {} + +s1Val := S1{} +s1Ptr := &S1{} +s2Val := S2{} +s2Ptr := &S2{} + +var i F +i = s1Val +i = s1Ptr +i = s2Ptr + +// 下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器 +// i = s2Val +``` + +[Effective Go](https://golang.org/doc/effective_go.html) 中有一段关于 [pointers vs. values](https://golang.org/doc/effective_go.html#pointers_vs_values) 的精彩讲解。 + +补充: + +- 一个类型可以有值接收器方法集和指针接收器方法集 + - 值接收器方法集是指针接收器方法集的子集,反之不是 +- 规则 + - 值对象只可以使用值接收器方法集 + - 指针对象可以使用 值接收器方法集 + 指针接收器方法集 +- 接口的匹配 (或者叫实现) + - 类型实现了接口的所有方法,叫匹配 + - 具体的讲,要么是类型的值方法集匹配接口,要么是指针方法集匹配接口 + +具体的匹配分两种: + +- 值方法集和接口匹配 + - 给接口变量赋值的不管是值还是指针对象,都 ok,因为都包含值方法集 +- 指针方法集和接口匹配 + - 只能将指针对象赋值给接口变量,因为只有指针方法集和接口匹配 + - 如果将值对象赋值给接口变量,会在编译期报错 (会触发接口合理性检查机制) + +为啥 i = s2Val 会报错,因为值方法集和接口不匹配。 + +### 零值 Mutex 是有效的 + +零值 `sync.Mutex` 和 `sync.RWMutex` 是有效的。所以指向 mutex 的指针基本是不必要的。 + + + + + +
BadGood
+ +```go +mu := new(sync.Mutex) +mu.Lock() +``` + + + +```go +var mu sync.Mutex +mu.Lock() +``` + +
+ +如果你使用结构体指针,mutex 应该作为结构体的非指针字段。即使该结构体不被导出,也不要直接把 mutex 嵌入到结构体中。 + + + + + + +
BadGood
+ +```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` 的实现细节,对其调用者不可见。 + +
+ +### 在边界处拷贝 Slices 和 Maps + +slices 和 maps 包含了指向底层数据的指针,因此在需要复制它们时要特别注意。 + +#### 接收 Slices 和 Maps + +请记住,当 map 或 slice 作为函数参数传入时,如果您存储了对它们的引用,则用户可以对其进行修改。 + + + + + + + + + + +
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] = ... +``` + +
+ +#### 返回 slices 或 maps + +同样,请注意用户对暴露内部状态的 map 或 slice 的修改。 + + + + + +
BadGood
+ +```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() +``` + +
+ +### 使用 defer 释放资源 + +使用 defer 释放资源,诸如文件和锁。 + + + + + +
BadGood
+ +```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 + +// 更可读 +``` + +
+ +Defer 的开销非常小,只有在您可以证明函数执行时间处于纳秒级的程度时,才应避免这样做。使用 defer 提升可读性是值得的,因为使用它们的成本微不足道。尤其适用于那些不仅仅是简单内存访问的较大的方法,在这些方法中其他计算的资源消耗远超过 `defer`。 + +### Channel 的 size 要么是 1,要么是无缓冲的 + +channel 通常 size 应为 1 或是无缓冲的。默认情况下,channel 是无缓冲的,其 size 为零。任何其他尺寸都必须经过严格的审查。我们需要考虑如何确定大小,考虑是什么阻止了 channel 在高负载下和阻塞写时的写入,以及当这种情况发生时系统逻辑有哪些变化。(翻译解释:按照原文意思是需要界定通道边界,竞态条件,以及逻辑上下文梳理) + + + + + +
BadGood
+ +```go +// 应该足以满足任何情况! +c := make(chan int, 64) +``` + + + +```go +// 大小:1 +c := make(chan int, 1) // 或者 +// 无缓冲 channel,大小为 0 +c := make(chan int) +``` + +
+ +### 枚举从 1 开始 + +在 Go 中引入枚举的标准方法是声明一个自定义类型和一个使用了 iota 的 const 组。由于变量的默认值为 0,因此通常应以非零值开头枚举。 + + + + + +
BadGood
+ +```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 +``` + +
+ +在某些情况下,使用零值是有意义的(枚举从零开始),例如,当零值是理想的默认行为时。 + +```go +type LogOutput int + +const ( + LogToStdout LogOutput = iota + LogToFile + LogToRemote +) + +// LogToStdout=0, LogToFile=1, LogToRemote=2 +``` + +### 使用 time 处理时间 + +时间处理很复杂。关于时间的错误假设通常包括以下几点。 + +1. 一天有 24 小时 +2. 一小时有 60 分钟 +3. 一周有七天 +4. 一年 365 天 +5. [还有更多](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time) + +例如,*1* 表示在一个时间点上加上 24 小时并不总是产生一个新的日历日。 + +因此,在处理时间时始终使用 [`"time"`] 包,因为它有助于以更安全、更准确的方式处理这些不正确的假设。 + +[`"time"`]: https://golang.org/pkg/time/ + +#### 使用 `time.Time` 表达瞬时时间 + +在处理时间的瞬间时使用 [`time.Time`],在比较、添加或减去时间时使用 `time.Time` 中的方法。 + +[`time.Time`]: https://golang.org/pkg/time/#Time + + + + + +
BadGood
+ +```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) +} +``` + +
+ +#### 使用 `time.Duration` 表达时间段 + +在处理时间段时使用 [`time.Duration`] . + +[`time.Duration`]: https://golang.org/pkg/time/#Duration + + + + + +
BadGood
+ +```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) +``` + +
+ +回到第一个例子,在一个时间瞬间加上 24 小时,我们用于添加时间的方法取决于意图。如果我们想要下一个日历日 (当前天的下一天) 的同一个时间点,我们应该使用 [`Time.AddDate`]。但是,如果我们想保证某一时刻比前一时刻晚 24 小时,我们应该使用 [`Time.Add`]。 + +[`Time.AddDate`]: https://golang.org/pkg/time/#Time.AddDate +[`Time.Add`]: https://golang.org/pkg/time/#Time.Add + +```go +newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */) +maybeNewDay := t.Add(24 * time.Hour) +``` + +#### 对外部系统使用 `time.Time` 和 `time.Duration` + +尽可能在与外部系统的交互中使用 `time.Duration` 和 `time.Time` 例如 : + +- Command-line 标志: [`flag`] 通过 [`time.ParseDuration`] 支持 `time.Duration` +- JSON: [`encoding/json`] 通过其 [`UnmarshalJSON` method] 方法支持将 `time.Time` 编码为 [RFC 3339] 字符串 +- SQL: [`database/sql`] 支持将 `DATETIME` 或 `TIMESTAMP` 列转换为 `time.Time`,如果底层驱动程序支持则返回 +- YAML: [`gopkg.in/yaml.v2`] 支持将 `time.Time` 作为 [RFC 3339] 字符串,并通过 [`time.ParseDuration`] 支持 `time.Duration`。 + + [`flag`]: https://golang.org/pkg/flag/ + [`time.ParseDuration`]: https://golang.org/pkg/time/#ParseDuration + [`encoding/json`]: https://golang.org/pkg/encoding/json/ + [RFC 3339]: https://tools.ietf.org/html/rfc3339 + [`UnmarshalJSON` method]: https://golang.org/pkg/time/#Time.UnmarshalJSON + [`database/sql`]: https://golang.org/pkg/database/sql/ + [`gopkg.in/yaml.v2`]: https://godoc.org/gopkg.in/yaml.v2 + +当不能在这些交互中使用 `time.Duration` 时,请使用 `int` 或 `float64`,并在字段名称中包含单位。 + +例如,由于 `encoding/json` 不支持 `time.Duration`,因此该单位包含在字段的名称中。 + + + + + +
BadGood
+ +```go +// {"interval": 2} +type Config struct { + Interval int `json:"interval"` +} +``` + + + +```go +// {"intervalMillis": 2000} +type Config struct { + IntervalMillis int `json:"intervalMillis"` +} +``` + +
+ +当在这些交互中不能使用 `time.Time` 时,除非达成一致,否则使用 `string` 和 [RFC 3339] 中定义的格式时间戳。默认情况下,[`Time.UnmarshalText`] 使用此格式,并可通过 [`time.RFC3339`] 在 `Time.Format` 和 `time.Parse` 中使用。 + +[`Time.UnmarshalText`]: https://golang.org/pkg/time/#Time.UnmarshalText +[`time.RFC3339`]: https://golang.org/pkg/time/#RFC3339 + +尽管这在实践中并不成问题,但请记住,`"time"` 包不支持解析闰秒时间戳([8728]),也不在计算中考虑闰秒([15190])。如果您比较两个时间瞬间,则差异将不包括这两个瞬间之间可能发生的闰秒。 + +[8728]: https://github.com/golang/go/issues/8728 +[15190]: https://github.com/golang/go/issues/15190 + + + + +### Errors + +#### 错误类型 + +声明错误的选项很少。 +在选择最适合您的用例的选项之前,请考虑以下事项。 + +- 调用者是否需要匹配错误以便他们可以处理它? + 如果是,我们必须通过声明顶级错误变量或自定义类型来支持 [`errors.Is`] 或 [`errors.As`] 函数。 +- 错误消息是否为静态字符串,还是需要上下文信息的动态字符串? + 如果是静态字符串,我们可以使用 [`errors.New`],但对于后者,我们必须使用 [`fmt.Errorf`] 或自定义错误类型。 +- 我们是否正在传递由下游函数返回的新错误? + 如果是这样,请参阅[错误包装部分](#错误包装)。 + +[`errors.Is`]: https://golang.org/pkg/errors/#Is +[`errors.As`]: https://golang.org/pkg/errors/#As + +| 错误匹配?| 错误消息 | 指导 | +|-----------------|---------------|-------------------------------------| +| No | static | [`errors.New`] | +| No | dynamic | [`fmt.Errorf`] | +| Yes | static | top-level `var` with [`errors.New`] | +| Yes | dynamic | custom `error` type | + +[`errors.New`]: https://golang.org/pkg/errors/#New +[`fmt.Errorf`]: https://golang.org/pkg/fmt/#Errorf + +例如, +使用 [`errors.New`] 表示带有静态字符串的错误。 +如果调用者需要匹配并处理此错误,则将此错误导出为变量以支持将其与 `errors.Is` 匹配。 + + + + + +
无错误匹配错误匹配
+ +```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") + } +} +``` + +
+ +对于动态字符串的错误, +如果调用者不需要匹配它,则使用 [`fmt.Errorf`], +如果调用者确实需要匹配它,则自定义 `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") + } +} +``` + +
+ +请注意,如果您从包中导出错误变量或类型, +它们将成为包的公共 API 的一部分。 + +#### 错误包装 + +如果调用其他方法时出现错误, 通常有三种处理方式可以选择: + +- 将原始错误原样返回 +- 使用 `fmt.Errorf` 搭配 `%w` 将错误添加进上下文后返回 +- 使用 `fmt.Errorf` 搭配 `%v` 将错误添加进上下文后返回 + +如果没有要添加的其他上下文,则按原样返回原始错误。 +这将保留原始错误类型和消息。 +这非常适合底层错误消息有足够的信息来追踪它来自哪里的错误。 + +否则,尽可能在错误消息中添加上下文 +这样就不会出现诸如“连接被拒绝”之类的模糊错误, +您会收到更多有用的错误,例如“调用服务 foo:连接被拒绝”。 + +使用 `fmt.Errorf` 为你的错误添加上下文, +根据调用者是否应该能够匹配和提取根本原因,在 `%w` 或 `%v` 动词之间进行选择。 + +- 如果调用者应该可以访问底层错误,请使用 `%w`。 + 对于大多数包装错误,这是一个很好的默认值, + 但请注意,调用者可能会开始依赖此行为。因此,对于包装错误是已知`var`或类型的情况,请将其作为函数契约的一部分进行记录和测试。 +- 使用 `%v` 来混淆底层错误。 + 调用者将无法匹配它,但如果需要,您可以在将来切换到 `%w`。 + +在为返回的错误添加上下文时,通过避免使用"failed to"之类的短语来保持上下文简洁,当错误通过堆栈向上渗透时,它会一层一层被堆积起来: + + + + + +
BadGood
+ +```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 +``` + +
+ +然而,一旦错误被发送到另一个系统,应该清楚消息是一个错误(例如`err` 标签或日志中的"Failed"前缀)。 + + +另见 [不要只检查错误,优雅地处理它们]。 + +[`"pkg/errors".Cause`]: https://godoc.org/github.com/pkg/errors#Cause +[不要只检查错误,优雅地处理它们]: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully + +#### 错误命名 + +对于存储为全局变量的错误值, +根据是否导出,使用前缀 `Err` 或 `err`。 +请看指南 [对于未导出的顶层常量和变量,使用_作为前缀](#对于未导出的顶层常量和变量使用_作为前缀)。 + +```go +var ( + // 导出以下两个错误,以便此包的用户可以将它们与 errors.Is 进行匹配。 + + ErrBrokenLink = errors.New("link is broken") + ErrCouldNotOpen = errors.New("could not open") + + // 这个错误没有被导出,因为我们不想让它成为我们公共 API 的一部分。 我们可能仍然在带有错误的包内使用它。 + + errNotFound = errors.New("not found") +) +``` + +对于自定义错误类型,请改用后缀 `Error`。 + +```go +// 同样,这个错误被导出,以便这个包的用户可以将它与 errors.As 匹配。 + +type NotFoundError struct { + File string +} + +func (e *NotFoundError) Error() string { + return fmt.Sprintf("file %q not found", e.File) +} + +// 并且这个错误没有被导出,因为我们不想让它成为公共 API 的一部分。 我们仍然可以在带有 errors.As 的包中使用它。 +type resolveError struct { + Path string +} + +func (e *resolveError) Error() string { + return fmt.Sprintf("resolve %q", e.Path) +} +``` +#### 一次处理错误 + +当调用方从被调用方接收到错误时,它可以根据对错误的了解,以各种不同的方式进行处理。 + +其中包括但不限于: + +- 如果被调用者约定定义了特定的错误,则将错误与`errors.Is`或`errors.As`匹配,并以不同的方式处理分支 +- 如果错误是可恢复的,则记录错误并正常降级 +- 如果该错误表示特定于域的故障条件,则返回定义明确的错误 +- 返回错误,无论是 [wrapped](#错误包装) 还是逐字逐句 + +无论调用方如何处理错误,它通常都应该只处理每个错误一次。例如,调用方不应该记录错误然后返回,因为*its*调用方也可能处理错误。 + +例如,考虑以下情况: + + + + + + + + +
DescriptionCode
+ +**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) + } +} +``` + +
+ +### 处理断言失败 + +[类型断言] 将会在检测到不正确的类型时,以单一返回值形式返回 panic。 因此,请始终使用“逗号 ok”习语。 + +[类型断言]: https://golang.org/ref/spec#Type_assertions + + + + + +
BadGood
+ +```go +t := i.(string) +``` + + + +```go +t, ok := i.(string) +if !ok { + // 优雅地处理错误 +} +``` + +
+ + + +### 不要使用 panic + +在生产环境中运行的代码必须避免出现 panic。panic 是 [级联失败] 的主要根源 。如果发生错误,该函数必须返回错误,并允许调用方决定如何处理它。 + +[级联失败]: https://en.wikipedia.org/wiki/Cascading_failure + + + + + +
BadGood
+ +```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) + } +} +``` + +
+ +panic/recover 不是错误处理策略。仅当发生不可恢复的事情(例如:nil 引用)时,程序才必须 panic。程序初始化是一个例外:程序启动时应使程序中止的不良情况可能会引起 panic。 + +```go +var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML")) +``` + +即使在测试代码中,也优先使用`t.Fatal`或者`t.FailNow`而不是 panic 来确保失败被标记。 + + + + + +
BadGood
+ +```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") +} +``` + +
+ + + +### 使用 go.uber.org/atomic + +使用 [sync/atomic] 包的原子操作对原始类型 (`int32`, `int64`等)进行操作,因为很容易忘记使用原子操作来读取或修改变量。 + +[go.uber.org/atomic] 通过隐藏基础类型为这些操作增加了类型安全性。此外,它包括一个方便的`atomic.Bool`类型。 + +[go.uber.org/atomic]: https://godoc.org/go.uber.org/atomic +[sync/atomic]: https://golang.org/pkg/sync/atomic/ + + + + + +
BadGood
+ +```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() +} +``` + +
+ +### 避免可变全局变量 + +使用选择依赖注入方式避免改变全局变量。 +既适用于函数指针又适用于其他值类型 + + + + + + +
BadGood
+ +```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)) +} +``` + +
+ +### 避免在公共结构中嵌入类型 + +这些嵌入的类型泄漏实现细节、禁止类型演化和模糊的文档。 + +假设您使用共享的 `AbstractList` 实现了多种列表类型,请避免在具体的列表实现中嵌入 `AbstractList`。 +相反,只需手动将方法写入具体的列表,该列表将委托给抽象列表。 + +```go +type AbstractList struct {} +// 添加将实体添加到列表中。 +func (l *AbstractList) Add(e Entity) { + // ... +} +// 移除从列表中移除实体。 +func (l *AbstractList) Remove(e Entity) { + // ... +} +``` + + + + + +
BadGood
+ +```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) +} +``` + +
+ +Go 允许 [类型嵌入](https://golang.org/doc/effective_go.html#embedding) 作为继承和组合之间的折衷。外部类型获取嵌入类型的方法的隐式副本。默认情况下,这些方法委托给嵌入实例的同一方法。 + +结构还获得与类型同名的字段。 +所以,如果嵌入的类型是 public,那么字段是 public。为了保持向后兼容性,外部类型的每个未来版本都必须保留嵌入类型。 + +很少需要嵌入类型。 +这是一种方便,可以帮助您避免编写冗长的委托方法。 + +即使嵌入兼容的抽象列表 *interface*,而不是结构体,这将为开发人员提供更大的灵活性来改变未来,但仍然泄露了具体列表使用抽象实现的细节。 + + + + + +
BadGood
+ +```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) +} +``` + +
+ +无论是使用嵌入结构还是嵌入接口,都会限制类型的演化。 + +- 向嵌入接口添加方法是一个破坏性的改变。 +- 从嵌入结构体删除方法是一个破坏性改变。 +- 删除嵌入类型是一个破坏性的改变。 +- 即使使用满足相同接口的类型替换嵌入类型,也是一个破坏性的改变。 + +尽管编写这些委托方法是乏味的,但是额外的工作隐藏了实现细节,留下了更多的更改机会,还消除了在文档中发现完整列表接口的间接性操作。 + +### 避免使用内置名称 + +Go [语言规范] 概述了几个内置的, +不应在 Go 项目中使用的 [预先声明的标识符]。 + +根据上下文的不同,将这些标识符作为名称重复使用, +将在当前作用域(或任何嵌套作用域)中隐藏原始标识符,或者混淆代码。 +在最好的情况下,编译器会报错;在最坏的情况下,这样的代码可能会引入潜在的、难以恢复的错误。 + +[语言规范]: https://golang.org/ref/spec +[预先声明的标识符]: https://golang.org/ref/spec#Predeclared_identifiers + + + + + + +
BadGood
+ +```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 +} +``` +
+ +注意,编译器在使用预先分隔的标识符时不会生成错误, +但是诸如`go vet`之类的工具会正确地指出这些和其他情况下的隐式问题。 + +### 避免使用 `init()` + +尽可能避免使用`init()`。当`init()`是不可避免或可取的,代码应先尝试: + +1. 无论程序环境或调用如何,都要完全确定。 +2. 避免依赖于其他`init()`函数的顺序或副作用。虽然`init()`顺序是明确的,但代码可以更改, +因此`init()`函数之间的关系可能会使代码变得脆弱和容易出错。 +3. 避免访问或操作全局或环境状态,如机器信息、环境变量、工作目录、程序参数/输入等。 +4. 避免`I/O`,包括文件系统、网络和系统调用。 + +不能满足这些要求的代码可能属于要作为`main()`调用的一部分(或程序生命周期中的其他地方), +或者作为`main()`本身的一部分写入。特别是,打算由其他程序使用的库应该特别注意完全确定性, +而不是执行“init magic” + + + + + + +
BadGood
+ +```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 +} +``` + +
+ +考虑到上述情况,在某些情况下,`init()`可能更可取或是必要的,可能包括: + +- 不能表示为单个赋值的复杂表达式。 +- 可插入的钩子,如`database/sql`、编码类型注册表等。 +- 对 [Google Cloud Functions] 和其他形式的确定性预计算的优化。 + + [Google Cloud Functions]: https://cloud.google.com/functions/docs/bestpractices/tips#use_global_variables_to_reuse_objects_in_future_invocations + +### 追加时优先指定切片容量 + +追加时优先指定切片容量 + +在尽可能的情况下,在初始化要追加的切片时为`make()`提供一个容量值。 + + + + + + +
BadGood
+ +```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 +``` + +
+ +### 主函数退出方式 (Exit) + +Go 程序使用 [`os.Exit`] 或者 [`log.Fatal*`] 立即退出 (使用`panic`不是退出程序的好方法,请 [不要使用 panic](#不要使用-panic)。) + +[`os.Exit`]: https://golang.org/pkg/os/#Exit +[`log.Fatal*`]: https://golang.org/pkg/log/#Fatal + +**仅在`main()`** 中调用其中一个 `os.Exit` 或者 `log.Fatal*`。所有其他函数应将错误返回到信号失败中。 + + + + + +
BadGood
+ +```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 +} +``` + +
+ +原则上:退出的具有多种功能的程序存在一些问题: + +- 不明显的控制流:任何函数都可以退出程序,因此很难对控制流进行推理。 +- 难以测试:退出程序的函数也将退出调用它的测试。这使得函数很难测试,并引入了跳过 `go test` 尚未运行的其他测试的风险。 +- 跳过清理:当函数退出程序时,会跳过已经进入`defer`队列里的函数调用。这增加了跳过重要清理任务的风险。 +#### 一次性退出 + +如果可能的话,你的`main()`函数中 **最多一次** 调用 `os.Exit`或者`log.Fatal`。如果有多个错误场景停止程序执行,请将该逻辑放在单独的函数下并从中返回错误。 +这会缩短 `main()` 函数,并将所有关键业务逻辑放入一个单独的、可测试的函数中。 + + + + + +
BadGood
+ +```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 + } + // ... +} +``` + +
+ +### 在序列化结构中使用字段标记 + +任何序列化到JSON、YAML、, +或其他支持基于标记的字段命名的格式应使用相关标记进行注释。 + + + + + +
BadGood
+ +```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", +}) +``` + +
+ +理论上: +结构的序列化形式是不同系统之间的契约。 +对序列化表单结构(包括字段名)的更改会破坏此约定。在标记中指定字段名使约定明确, +它还可以通过重构或重命名字段来防止意外违反约定。 + +### 不要一劳永逸地使用 goroutine + +Goroutines 是轻量级的,但它们不是免费的: +至少,它们会为堆栈和 CPU 的调度消耗内存。 +虽然这些成本对于 Goroutines 的使用来说很小,但当它们在没有受控生命周期的情况下大量生成时会导致严重的性能问题。 +具有非托管生命周期的 Goroutines 也可能导致其他问题,例如防止未使用的对象被垃圾回收并保留不再使用的资源。 + +因此,不要在代码中泄漏 goroutine。 +使用 [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak) +来测试可能产生 goroutine 的包内的 goroutine 泄漏。 + +一般来说,每个 goroutine: + +- 必须有一个可预测的停止运行时间; 或者 +- 必须有一种方法可以向 goroutine 发出信号它应该停止 + +在这两种情况下,都必须有一种方式代码来阻塞并等待 goroutine 完成。 + +For example: + + + + + + +
BadGood
+ +```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`. + +
+ +#### 等待 goroutines 退出 + +给定一个由系统生成的 goroutine, +必须有一种方案能等待 goroutine 的退出。 +有两种常用的方法可以做到这一点: + +- 使用 `sync.WaitGroup`. + 如果您要等待多个 goroutine,请执行此操作 + + ```go + var wg sync.WaitGroup + for i := 0; i < N; i++ { + wg.Add(1) + go func() { + defer wg.Done() + // ... + }() + } + + // To wait for all to finish: + wg.Wait() + ``` + +- 添加另一个 `chan struct{}`,goroutine 完成后会关闭它。 + 如果只有一个 goroutine,请执行此操作。 + + ```go + done := make(chan struct{}) + go func() { + defer close(done) + // ... + }() + + // To wait for the goroutine to finish: + <-done + ``` + +#### 不要在 `init()` 使用 goroutines + +`init()` 函数不应该产生 goroutines。 +另请参阅 [避免使用 init()](#避免使用-init)。 + +如果一个包需要一个后台 goroutine, +它必须公开一个负责管理 goroutine 生命周期的对象。 +该对象必须提供一个方法(`Close`、`Stop`、`Shutdown` 等)来指示后台 goroutine 停止并等待它的退出。 + + + + + + +
BadGood
+ +```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-退出)。 + + +
+ +## 性能 + +性能方面的特定准则只适用于高频场景。 + +### 优先使用 strconv 而不是 fmt + +将原语转换为字符串或从字符串转换时,`strconv`速度比`fmt`快。 + + + + + + +
BadGood
+ +```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 +``` + +
+ + + +### 避免字符串到字节的转换 + +不要反复从固定字符串创建字节 slice。相反,请执行一次转换并捕获结果。 + + + + + + +
BadGood
+ +```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 +``` + +
+ +### 指定容器容量 + +尽可能指定容器容量,以便为容器预先分配内存。这将在添加元素时最小化后续分配(通过复制和调整容器大小)。 + +#### 指定 Map 容量提示 + +在尽可能的情况下,在使用 `make()` 初始化的时候提供容量信息 + +```go +make(map[T1]T2, hint) +``` + +向`make()`提供容量提示会在初始化时尝试调整 map 的大小,这将减少在将元素添加到 map 时为 map 重新分配内存。 + + +注意,与 slices 不同。map 容量提示并不保证完全的、预先的分配,而是用于估计所需的 hashmap bucket 的数量。 +因此,在将元素添加到 map 时,甚至在指定 map 容量时,仍可能发生分配。 + + + + + + +
BadGood
+ +```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` 是有大小提示创建的;在运行时可能会有更少的分配。 + +
+ +#### 指定切片容量 + +在尽可能的情况下,在使用`make()`初始化切片时提供容量信息,特别是在追加切片时。 + +```go +make([]T, length, capacity) +``` + +与 maps 不同,slice capacity 不是一个提示:编译器将为提供给`make()`的 slice 的容量分配足够的内存, +这意味着后续的`append()`操作将导致零分配(直到 slice 的长度与容量匹配,在此之后,任何 append 都可能调整大小以容纳其他元素)。 + + + + + + +
BadGood
+ +```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 +``` + +
+ +## 规范 +### 避免过长的行 + +避免使用需要读者水平滚动或过度转动头部的代码行。 + +我们建议将行长度限制为 **99 characters** (99 个字符). +作者应该在达到这个限制之前换行, +但这不是硬性限制。 +允许代码超过此限制。 +### 一致性 + +本文中概述的一些标准都是客观性的评估,是根据场景、上下文、或者主观性的判断; + +但是最重要的是,**保持一致**. + +一致性的代码更容易维护、是更合理的、需要更少的学习成本、并且随着新的约定出现或者出现错误后更容易迁移、更新、修复 bug + +相反,在一个代码库中包含多个完全不同或冲突的代码风格会导致维护成本开销、不确定性和认知偏差。所有这些都会直接导致速度降低、代码审查痛苦、而且增加 bug 数量。 + +将这些标准应用于代码库时,建议在 package(或更大)级别进行更改,子包级别的应用程序通过将多个样式引入到同一代码中,违反了上述关注点。 + +### 相似的声明放在一组 + +Go 语言支持将相似的声明放在一个组内。 + + + + + +
BadGood
+ +```go +import "a" +import "b" +``` + + + +```go +import ( + "a" + "b" +) +``` + +
+ +这同样适用于常量、变量和类型声明: + + + + + +
BadGood
+ +```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 +) +``` + +
+ +仅将相关的声明放在一组。不要将不相关的声明放在一组。 + + + + + +
BadGood
+ +```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" +``` + +
+ +分组使用的位置没有限制,例如:你可以在函数内部使用它们: + + + + + +
BadGood
+ +```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) + ) + + ... +} +``` + +
+ +例外:如果变量声明与其他变量相邻,则应将变量声明(尤其是函数内部的声明)分组在一起。对一起声明的变量执行此操作,即使它们不相关。 + + + + + +
BadGood
+ +```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 + ) + // ... +} +``` + +
+ +### import 分组 + +导入应该分为两组: + +- 标准库 +- 其他库 + +默认情况下,这是 goimports 应用的分组。 + + + + + +
BadGood
+ +```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" +) +``` + +
+ +### 包名 + +当命名包时,请按下面规则选择一个名称: + +- 全部小写。没有大写或下划线。 +- 大多数使用命名导入的情况下,不需要重命名。 +- 简短而简洁。请记住,在每个使用的地方都完整标识了该名称。 +- 不用复数。例如`net/url`,而不是`net/urls`。 +- 不要用“common”,“util”,“shared”或“lib”。这些是不好的,信息量不足的名称。 + +另请参阅 [Go 包命名规则] 和 [Go 包样式指南]. + +[Go 包命名规则]: https://blog.golang.org/package-names +[Go 包样式指南]: https://rakyll.org/style-packages/ + +### 函数名 + +我们遵循 Go 社区关于使用 [MixedCaps 作为函数名] 的约定。有一个例外,为了对相关的测试用例进行分组,函数名可能包含下划线,如:`TestMyFunction_WhatIsBeingTested`. + +[MixedCaps 作为函数名]: https://golang.org/doc/effective_go.html#mixed-caps + +### 导入别名 + +如果程序包名称与导入路径的最后一个元素不匹配,则必须使用导入别名。 + +```go +import ( + "net/http" + + client "example.com/client-go" + trace "example.com/trace/v2" +) +``` + +在所有其他情况下,除非导入之间有直接冲突,否则应避免导入别名。 + + + + + +
BadGood
+ +```go +import ( + "fmt" + "os" + + nettrace "golang.net/x/trace" +) +``` + + + +```go +import ( + "fmt" + "os" + "runtime/trace" + + nettrace "golang.net/x/trace" +) +``` + +
+ +### 函数分组与顺序 + +- 函数应按粗略的调用顺序排序。 +- 同一文件中的函数应按接收者分组。 + +因此,导出的函数应先出现在文件中,放在`struct`, `const`, `var`定义的后面。 + +在定义类型之后,但在接收者的其余方法之前,可能会出现一个 `newXYZ()`/`NewXYZ()` + +由于函数是按接收者分组的,因此普通工具函数应在文件末尾出现。 + + + + + +
BadGood
+ +```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 {...} +``` + +
+ +### 减少嵌套 + +代码应通过尽可能先处理错误情况/特殊情况并尽早返回或继续循环来减少嵌套。减少嵌套多个级别的代码的代码量。 + + + + + +
BadGood
+ +```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() +} +``` + +
+ +### 不必要的 else + +如果在 if 的两个分支中都设置了变量,则可以将其替换为单个 if。 + + + + + +
BadGood
+ +```go +var a int +if b { + a = 100 +} else { + a = 10 +} +``` + + + +```go +a := 10 +if b { + a = 100 +} +``` + +
+ +### 顶层变量声明 + +在顶层,使用标准`var`关键字。请勿指定类型,除非它与表达式的类型不同。 + + + + + +
BadGood
+ +```go +var _s string = F() + +func F() string { return "A" } +``` + + + +```go +var _s = F() +// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型 +// 还是那种类型 + +func F() string { return "A" } +``` + +
+ +如果表达式的类型与所需的类型不完全匹配,请指定类型。 + +```go +type myError struct{} + +func (myError) Error() string { return "error" } + +func F() myError { return myError{} } + +var _e error = F() +// F 返回一个 myError 类型的实例,但是我们要 error 类型 +``` + +### 对于未导出的顶层常量和变量,使用_作为前缀 + +在未导出的顶级`vars`和`consts`, 前面加上前缀_,以使它们在使用时明确表示它们是全局符号。 + +基本依据:顶级变量和常量具有包范围作用域。使用通用名称可能很容易在其他文件中意外使用错误的值。 + + + + + +
BadGood
+ +```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" +) +``` + +
+ +**例外**:未导出的错误值可以使用不带下划线的前缀 `err`。 参见[错误命名](#错误命名)。 + +### 结构体中的嵌入 + +嵌入式类型(例如 mutex)应位于结构体内的字段列表的顶部,并且必须有一个空行将嵌入式字段与常规字段分隔开。 + + + + + +
BadGood
+ +```go +type Client struct { + version int + http.Client +} +``` + + + +```go +type Client struct { + http.Client + + version int +} +``` + +
+ +内嵌应该提供切实的好处,比如以语义上合适的方式添加或增强功能。 +它应该在对用户没有任何不利影响的情况下使用。(另请参见:[避免在公共结构中嵌入类型])。 + +例外:即使在未导出类型中,Mutex 也不应该作为内嵌字段。另请参见:[零值 Mutex 是有效的]。 + +[避免在公共结构中嵌入类型]: #避免在公共结构中嵌入类型 +[零值 Mutex 是有效的]: #零值-mutex-是有效的 + +嵌入 **不应该**: + +- 纯粹是为了美观或方便。 +- 使外部类型更难构造或使用。 +- 影响外部类型的零值。如果外部类型有一个有用的零值,则在嵌入内部类型之后应该仍然有一个有用的零值。 +- 作为嵌入内部类型的副作用,从外部类型公开不相关的函数或字段。 +- 公开未导出的类型。 +- 影响外部类型的复制形式。 +- 更改外部类型的 API 或类型语义。 +- 嵌入内部类型的非规范形式。 +- 公开外部类型的实现详细信息。 +- 允许用户观察或控制类型内部。 +- 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。 + +简单地说,有意识地和有目的地嵌入。一种很好的测试体验是, +"是否所有这些导出的内部方法/字段都将直接添加到外部类型" +如果答案是`some`或`no`,不要嵌入内部类型 - 而是使用字段。 + + + + + + + +
BadGood
+ +```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 +} +``` + +
+ +### 本地变量声明 + +如果将变量明确设置为某个值,则应使用短变量声明形式 (`:=`)。 + + + + + +
BadGood
+ +```go +var s = "foo" +``` + + + +```go +s := "foo" +``` + +
+ +但是,在某些情况下,`var` 使用关键字时默认值会更清晰。例如,[声明空切片]。 + +[声明空切片]: https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices + + + + + +
BadGood
+ +```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) + } + } +} +``` + +
+ +### nil 是一个有效的 slice + +`nil` 是一个有效的长度为 0 的 slice,这意味着, + +- 您不应明确返回长度为零的切片。应该返回`nil` 来代替。 + + + + + +
BadGood
+ + ```go + if x == "" { + return []int{} + } + ``` + + + + ```go + if x == "" { + return nil + } + ``` + +
+ +- 要检查切片是否为空,请始终使用`len(s) == 0`。而非 `nil`。 + + + + + +
BadGood
+ + ```go + func isEmpty(s []string) bool { + return s == nil + } + ``` + + + + ```go + func isEmpty(s []string) bool { + return len(s) == 0 + } + ``` + +
+ +- 零值切片(用`var`声明的切片)可立即使用,无需调用`make()`创建。 + + + + + +
BadGood
+ + ```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) + } + ``` + +
+ +记住,虽然 nil 切片是有效的切片,但它不等于长度为 0 的切片(一个为 nil,另一个不是),并且在不同的情况下(例如序列化),这两个切片的处理方式可能不同。 + +### 缩小变量作用域 + +如果有可能,尽量缩小变量作用范围。除非它与 [减少嵌套](#减少嵌套)的规则冲突。 + + + + + +
BadGood
+ +```go +err := os.WriteFile(name, data, 0644) +if err != nil { + return err +} +``` + + + +```go +if err := os.WriteFile(name, data, 0644); err != nil { + return err +} +``` + +
+ +如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。 + + + + + +
BadGood
+ +```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 +``` + +
+ +### 避免参数语义不明确 (Avoid Naked Parameters) + +函数调用中的`意义不明确的参数`可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (`/* ... */`) + + + + + +
BadGood
+ +```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 */) +``` + +
+ +对于上面的示例代码,还有一种更好的处理方式是将上面的 `bool` 类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)。 + +```go +type Region int + +const ( + UnknownRegion Region = iota + Local +) + +type Status int + +const ( + StatusReady Status= iota + 1 + StatusDone + // Maybe we will have a StatusInProgress in the future. +) + +func printInfo(name string, region Region, status Status) +``` + +### 使用原始字符串字面值,避免转义 + +Go 支持使用 [原始字符串字面值](https://golang.org/ref/spec#raw_string_lit),也就是 " ` " 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。 + +可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。 + + + + + +
BadGood
+ +```go +wantError := "unknown name:\"test\"" +``` + + + +```go +wantError := `unknown error:"test"` +``` + +
+ +### 初始化结构体 + +#### 使用字段名初始化结构 + +初始化结构时,几乎应该始终指定字段名。目前由 [`go vet`] 强制执行。 + +[`go vet`]: https://golang.org/cmd/vet/ + + + + + +
BadGood
+ +```go +k := User{"John", "Doe", true} +``` + + + +```go +k := User{ + FirstName: "John", + LastName: "Doe", + Admin: true, +} +``` + +
+ +例外:当有 3 个或更少的字段时,测试表中的字段名*may*可以省略。 + +```go +tests := []struct{ + op Operation + want string +}{ + {Add, "add"}, + {Subtract, "subtract"}, +} +``` +#### 省略结构中的零值字段 + +初始化具有字段名的结构时,除非提供有意义的上下文,否则忽略值为零的字段。 +也就是,让我们自动将这些设置为零值 + + + + + +
BadGood
+ +```go +user := User{ + FirstName: "John", + LastName: "Doe", + MiddleName: "", + Admin: false, +} +``` + + + +```go +user := User{ + FirstName: "John", + LastName: "Doe", +} +``` + +
+ +这有助于通过省略该上下文中的默认值来减少阅读的障碍。只指定有意义的值。 + +在字段名提供有意义上下文的地方包含零值。例如,[表驱动测试](#表驱动测试) 中的测试用例可以受益于字段的名称,即使它们是零值的。 + +```go +tests := []struct{ + give string + want int +}{ + {give: "0", want: 0}, + // ... +} +``` +#### 对零值结构使用 `var` + +如果在声明中省略了结构的所有字段,请使用 `var` 声明结构。 + + + + + +
BadGood
+ +```go +user := User{} +``` + + + +```go +var user User +``` + +
+ +这将零值结构与那些具有类似于为 [初始化 Maps](#初始化-maps) 创建的,区别于非零值字段的结构区分开来, +我们倾向于[声明一个空切片](https://github.com/golang/go/wiki/CodeReviewComments#declaring-empty-slices) + +#### 初始化 Struct 引用 + +在初始化结构引用时,请使用`&T{}`代替`new(T)`,以使其与结构体初始化一致。 + + + + + +
BadGood
+ +```go +sval := T{Name: "foo"} + +// inconsistent +sptr := new(T) +sptr.Name = "bar" +``` + + + +```go +sval := T{Name: "foo"} + +sptr := &T{Name: "bar"} +``` + +
+ +### 初始化 Maps + +对于空 map 请使用 `make(..)` 初始化, 并且 map 是通过编程方式填充的。 +这使得 map 初始化在表现上不同于声明,并且它还可以方便地在 make 后添加大小提示。 + + + + + + +
BadGood
+ +```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 +) +``` + +
+ +声明和初始化看起来非常相似的。 + + + +声明和初始化看起来差别非常大。 + +
+ +在尽可能的情况下,请在初始化时提供 map 容量大小,详细请看 [指定 Map 容量提示](#指定Map容量提示)。 + + +另外,如果 map 包含固定的元素列表,则使用 map literals(map 初始化列表) 初始化映射。 + + + + + + +
BadGood
+ +```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, +} +``` + +
+ +基本准则是:在初始化时使用 map 初始化列表 来添加一组固定的元素。否则使用 `make` (如果可以,请尽量指定 map 容量)。 + +### 字符串 string format + +如果你在函数外声明`Printf`-style 函数的格式字符串,请将其设置为`const`常量。 + +这有助于`go vet`对格式字符串执行静态分析。 + + + + + +
BadGood
+ +```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) +``` + +
+ +### 命名 Printf 样式的函数 + +声明`Printf`-style 函数时,请确保`go vet`可以检测到它并检查格式字符串。 + +这意味着您应尽可能使用预定义的`Printf`-style 函数名称。`go vet`将默认检查这些。有关更多信息,请参见 [Printf 系列]。 + +[Printf 系列]: https://golang.org/cmd/vet/#hdr-Printf_family + +如果不能使用预定义的名称,请以 f 结束选择的名称:`Wrapf`,而不是`Wrap`。`go vet`可以要求检查特定的 Printf 样式名称,但名称必须以`f`结尾。 + +```shell +go vet -printfuncs=wrapf,statusf +``` + +另请参阅 [go vet: Printf family check]. + +[go vet: Printf family check]: https://kuzminva.wordpress.com/2017/11/07/go-vet-printf-family-check/ + +## 编程模式 + +### 表驱动测试 + +当测试逻辑是重复的时候,通过 [subtests] 使用 table 驱动的方式编写 case 代码看上去会更简洁。 + +[subtests]: https://blog.golang.org/subtests + + + + + +
BadGood
+ +```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) + }) +} +``` + +
+ +很明显,使用 test table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。 + +我们遵循这样的约定:将结构体切片称为`tests`。 每个测试用例称为`tt`。此外,我们鼓励使用`give`和`want`前缀说明每个测试用例的输入和输出值。 + +```go +tests := []struct{ + give string + wantHost string + wantPort string +}{ + // ... +} + +for _, tt := range tests { + // ... +} +``` + +并行测试,比如一些专门的循环(例如,生成goroutine或捕获引用作为循环体的一部分的那些循环) +必须注意在循环的范围内显式地分配循环变量,以确保它们保持预期的值。 + +```go +tests := []struct{ + give string + // ... +}{ + // ... +} +for _, tt := range tests { + tt := tt // for t.Parallel + t.Run(tt.give, func(t *testing.T) { + t.Parallel() + // ... + }) +} +``` + +在上面的例子中,由于下面使用了`t.Parallel()`,我们必须声明一个作用域为循环迭代的`tt`变量。 +如果我们不这样做,大多数或所有测试都会收到一个意外的`tt`值,或者一个在运行时发生变化的值。 + +### 功能选项 + +功能选项是一种模式,您可以在其中声明一个不透明 Option 类型,该类型在某些内部结构中记录信息。您接受这些选项的可变编号,并根据内部结构上的选项记录的全部信息采取行动。 + +将此模式用于您需要扩展的构造函数和其他公共 API 中的可选参数,尤其是在这些功能上已经具有三个或更多参数的情况下。 + + + + + + +
BadGood
+ +```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), +) +``` + +
+ +我们建议实现此模式的方法是使用一个 `Option` 接口,该接口保存一个未导出的方法,在一个未导出的 `options` 结构上记录选项。 + +```go +type options struct { + cache bool + logger *zap.Logger +} + +type Option interface { + apply(*options) +} + +type cacheOption bool + +func (c cacheOption) apply(opts *options) { + opts.cache = bool(c) +} + +func WithCache(c bool) Option { + return cacheOption(c) +} + +type loggerOption struct { + Log *zap.Logger +} + +func (l loggerOption) apply(opts *options) { + opts.logger = l.Log +} + +func WithLogger(log *zap.Logger) Option { + return loggerOption{Log: log} +} + +// Open creates a connection. +func Open( + addr string, + opts ...Option, +) (*Connection, error) { + options := options{ + cache: defaultCache, + logger: zap.NewNop(), + } + + for _, o := range opts { + o.apply(&options) + } + + // ... +} +``` + +注意:还有一种使用闭包实现这个模式的方法,但是我们相信上面的模式为作者提供了更多的灵活性,并且更容易对用户进行调试和测试。特别是,在不可能进行比较的情况下它允许在测试和模拟中对选项进行比较。此外,它还允许选项实现其他接口,包括 `fmt.Stringer`,允许用户读取选项的字符串表示形式。 + +还可以参考下面资料: + +- [Self-referential functions and the design of options] +- [Functional options for friendly APIs] + + [Self-referential functions and the design of options]: https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html + [Functional options for friendly APIs]: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis + + + +## Linting + +比任何 "blessed" linter 集更重要的是,lint 在一个代码库中始终保持一致。 + +我们建议至少使用以下 linters,因为我认为它们有助于发现最常见的问题,并在不需要规定的情况下为代码质量建立一个高标准: + +- [errcheck] 以确保错误得到处理 +- [goimports] 格式化代码和管理 imports +- [golint] 指出常见的文体错误 +- [govet] 分析代码中的常见错误 +- [staticcheck] 各种静态分析检查 + + [errcheck]: https://github.com/kisielk/errcheck + [goimports]: https://godoc.org/golang.org/x/tools/cmd/goimports + [golint]: https://github.com/golang/lint + [govet]: https://golang.org/cmd/vet/ + [staticcheck]: https://staticcheck.io/ + + +### Lint Runners + +我们推荐 [golangci-lint] 作为 go-to lint 的运行程序,这主要是因为它在较大的代码库中的性能以及能够同时配置和使用许多规范。这个 repo 有一个示例配置文件 [.golangci.yml] 和推荐的 linter 设置。 + +golangci-lint 有 [various-linters] 可供使用。建议将上述 linters 作为基本 set,我们鼓励团队添加对他们的项目有意义的任何附加 linters。 + +[golangci-lint]: https://github.com/golangci/golangci-lint +[.golangci.yml]: https://github.com/uber-go/guide/blob/master/.golangci.yml +[various-linters]: https://golangci-lint.run/usage/linters/ diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" new file mode 100644 index 00000000..e69de29b diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteAction.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteAction.md" new file mode 100644 index 00000000..f04e4645 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteAction.md" @@ -0,0 +1,41 @@ +## /FavoriteAction + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/favorite/action?token=user_1password_1&video_id=1&action_type=1 + +#### 请求方式 + +> POST + +#### Content-Type + +> form-data + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| token | user_1password_1 | String | 是 | - | +| video_id | 1 | Integer | 是 | - | +| action_type | 1 | Integer | 是 | 1-点赞,2-取消点赞 | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteList.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteList.md" new file mode 100644 index 00000000..86d142d9 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/FavoriteList.md" @@ -0,0 +1,47 @@ +## /FavoriteList + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/favorite/list?user_id=1&token=user_1password_1 + +#### 请求方式 + +> GET + +#### Content-Type + +> form-data + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| user_id | 1 | String | 是 | - | +| token | user_1password_1 | String | 是 | - | + +#### 请求Body参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| user_id | - | Integer | 是 | - | +| token | - | String | 是 | 根据username和password生成 | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.md" new file mode 100644 index 00000000..b7d93cb0 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\345\210\227\350\241\250.md" @@ -0,0 +1,40 @@ +## /评论列表 + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/comment/list?token=user_1password&video_id=1 + +#### 请求方式 + +> GET + +#### Content-Type + +> none + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| token | user_1password | String | 是 | - | +| video_id | 1 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\346\223\215\344\275\234.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\346\223\215\344\275\234.md" new file mode 100644 index 00000000..2766fed6 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\344\272\222\345\212\250\345\212\237\350\203\275\346\265\201\347\250\213/\350\257\204\350\256\272\346\223\215\344\275\234.md" @@ -0,0 +1,42 @@ +## /评论操作 + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/comment/action?token=user_1password_1&video_id=1&action_type=1&comment_text=你好 + +#### 请求方式 + +> POST + +#### Content-Type + +> none + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| token | user_1password_1 | String | 是 | - | +| video_id | 1 | String | 是 | - | +| action_type | 1 | String | 是 | 1-点赞,2-取消点赞 | +| comment_text | 你好 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" new file mode 100644 index 00000000..4a70291b --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" @@ -0,0 +1,96 @@ +## 全局公共参数 +#### 全局Header参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Query参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Body参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局认证方式 +```text +noauth +``` +#### 全局预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 全局后执行脚本 +```javascript +暂无后执行脚本 +``` +## /发布列表 +```text +暂无描述 +``` +#### 接口状态 +> 开发中 + +#### 接口URL +> http://localhost:8080/douyin/publish/list/?token=&user_id= + +#### 请求方式 +> GET + +#### Content-Type +> none + +#### 请求Query参数 + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |user_1password_1 | String | 是 | 正确token +user_id | user_1 | String | 是 | 正确id + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token | 张三 | String | 是 | 错误token +user_id | user_1 | String | 是 | - + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token | - | String | 是 | 空缺token +user_id | user_1 | String | 是 | - + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |user_2password_1 | String | 是 | 错误token +user_id | user_1 | String | 是 | 正确id + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |user_1password_1 | String | 是 | 正确token +user_id | user_2 | String | 是 | 错误id + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |user_1password_1 | String | 是 | 正确token +user_id | - | String | 是 | 空缺 + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |user_1password_1 | String | 是 | 正确token +user_id | ][][] | String | 是 | 错误id + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +token |$$$$$$$ | String | 是 | 错误token +user_id | $$ | String | 是 | 错误id + + +#### 认证方式 +```text +noauth +``` +#### 预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 后执行脚本 +```javascript +暂无后执行脚本 +``` \ No newline at end of file diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\346\212\225\347\250\277\346\216\245\345\217\243.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\346\212\225\347\250\277\346\216\245\345\217\243.md" new file mode 100644 index 00000000..56caeca7 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\345\237\272\347\241\200\345\212\237\350\203\275\346\265\201\347\250\213/\346\212\225\347\250\277\346\216\245\345\217\243.md" @@ -0,0 +1,59 @@ +## 全局公共参数 +#### 全局Header参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Query参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Body参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局认证方式 +```text +noauth +``` +#### 全局预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 全局后执行脚本 +```javascript +暂无后执行脚本 +``` +## /投稿接口 +```text +暂无描述 +``` +#### 接口状态 +> 开发中 + +#### 接口URL +> http://localhost:8080/douyin/publish/action/ + +#### 请求方式 +> POST + +#### Content-Type +> form-data + +#### 请求Body参数 +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +data | - | Date | 是 | - +token | user_1password_1 | String | 是 | - +title | test | String | 是 | - +#### 认证方式 +```text +noauth +``` +#### 预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 后执行脚本 +```javascript +暂无后执行脚本 +``` \ No newline at end of file diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" new file mode 100644 index 00000000..87741bab --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" @@ -0,0 +1,40 @@ +## /用户信息 + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/user/?user_id=1&token=user_1password_1 + +#### 请求方式 + +> GET + +#### Content-Type + +> none + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| user_id | 1 | String | 是 | - | +| token | user_1password_1 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\357\274\210\350\207\252\346\265\213\357\274\211.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\357\274\210\350\207\252\346\265\213\357\274\211.md" new file mode 100644 index 00000000..08d1dd0f --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\357\274\210\350\207\252\346\265\213\357\274\211.md" @@ -0,0 +1,73 @@ +## /user info + +```text +UserInfo test +``` + +#### 接口状态 + +> 已完成 + +#### 接口URL + +> 127.0.0.1:8080/douyin/user/?user_id=1&token=user_1password_1 + +#### 请求方式 + +> GET + +#### Content-Type + +> urlencoded + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| user_id | 1 | Integer | 是 | - | +| token | user_1password_1 | String | 是 | - | + + +#### 成功响应示例 +```javascript +{ + "status_code": 0, + "user": { + "id": 1, + "name": "user_1", + "password": "password_1" + } +} +``` + +#### 接口URL + +127.0.0.1:8080/douyin/user/?user_id=888&token= + +#### 请求方式 + +> GET + +#### Content-Type + +> urlencoded + +#### 请求Query参数 +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| user_id | 888 | Integer | 是 | - | +| token | - | String | 是 | - | + +#### 错误响应示例 + +```javascript +{ + "status_code": 1, + "status_msg": "User doesn't exist", + "user": { + "id": 0, + "name": "", + "password": "" + } +} +``` \ No newline at end of file diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" new file mode 100644 index 00000000..fb7283d5 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" @@ -0,0 +1,40 @@ +## /用户注册 + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/user/register/?username=yunyin&password=123456 + +#### 请求方式 + +> POST + +#### Content-Type + +> none + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| username | yunyin | String | 是 | - | +| password | 123456 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" new file mode 100644 index 00000000..7fb5b58d --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" @@ -0,0 +1,40 @@ +## /用户登录 + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> http://127.0.0.1:8080/douyin/user/login/?username=user_1&password=password_1 + +#### 请求方式 + +> POST + +#### Content-Type + +> none + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| username | user_1 | String | 是 | - | +| password | password_1 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\346\216\245\345\217\243.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\346\216\245\345\217\243.md" new file mode 100644 index 00000000..df22cb1d --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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\346\216\245\345\217\243.md" @@ -0,0 +1,61 @@ +## /Login + +```text +暂无描述 +``` + +#### 接口状态 + +> 开发中 + +#### 接口URL + +> 127.0.0.1:8080/douyin/user/login/?username=user_1&password=password_1 + +#### 请求方式 + +> POST + +#### Content-Type + +> form-data + +#### 请求Query参数 + +| 参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 | +| --- | --- | ---- | ---- | ---- | +| username | user_1 | String | 是 | - | +| password | password_1 | String | 是 | - | + +#### 预执行脚本 + +```javascript +暂无预执行脚本 +``` + +#### 后执行脚本 + +```javascript +暂无后执行脚本 +``` + +#### 成功响应示例 + +```javascript +{ + "status_code": 0, + "status_msg": "登录成功!", + "user_id": 1, + "token": "user_1password_1" +} +``` + +#### 错误响应示例 + +```javascript +{ + "status_code": 1, + "status_msg": "登录失败!请检查用户名和密码。", + "token": "" +} +``` diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" new file mode 100644 index 00000000..a3eca6f9 --- /dev/null +++ "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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.md" @@ -0,0 +1,88 @@ +## 全局公共参数 +#### 全局Header参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Query参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局Body参数 +参数名 | 示例值 | 参数描述 +--- | --- | --- +暂无参数 +#### 全局认证方式 +```text +noauth +``` +#### 全局预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 全局后执行脚本 +```javascript +暂无后执行脚本 +``` +## /视频流接口 +```text +暂无描述 +``` +#### 接口状态 +> 开发中 + +#### 接口URL +> http://localhost:8080/douyin/feed/?latest_time=-1&token=user_1password_1 + +#### 请求方式 +> GET + +#### Content-Type +> none + +#### 请求Query参数 +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | -1 | String | 否 | 错误的time值 +token | user_1password_1 | String | 否 | - + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | 1693125045 | String | 否 | 正确的time值 +token | user_1password_1 | String | 否 | - + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | - | String | 否 | 空缺 +token | user_1password_1 | String | 否 | - + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | 1693125045 | String | 否 | 正确的time值 +token | - | String | 否 | 空缺 + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | - | String | 否 | 空缺 +token | - | String | 否 | 空缺 + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | 上个月 | String | 否 | 错误值 +token | user_1password_1 | String | 否 | 空缺 + +参数名 | 示例值 | 参数类型 | 是否必填 | 参数描述 +--- | --- | --- | --- | --- +latest_time | - | String | 否 | 空缺 +token | 张三 | String | 否 | 错误 +#### 认证方式 +```text +noauth +``` +#### 预执行脚本 +```javascript +暂无预执行脚本 +``` +#### 后执行脚本 +```javascript +暂无后执行脚本 +``` \ No newline at end of file diff --git "a/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\347\244\276\344\272\244\345\212\237\350\203\275\346\265\201\347\250\213/.gitkeep" "b/documents/03-\346\265\213\350\257\225\347\224\250\344\276\213/\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/go.mod b/go.mod deleted file mode 100644 index 186ab0fb..00000000 --- a/go.mod +++ /dev/null @@ -1,51 +0,0 @@ -module github.com/RaymondCode/simple-demo - -go 1.17 - -require ( - github.com/gavv/httpexpect/v2 v2.8.0 - github.com/gin-gonic/gin v1.7.7 - github.com/stretchr/testify v1.7.0 -) - -require ( - github.com/ajg/form v1.5.1 // indirect - github.com/andybalholm/brotli v1.0.4 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/structs v1.1.0 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator/v10 v10.11.0 // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/websocket v1.4.2 // indirect - github.com/imkira/go-interpol v1.1.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.15.0 // indirect - github.com/leodido/go-urn v1.2.1 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mitchellh/go-wordwrap v1.0.1 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/sanity-io/litter v1.5.5 // indirect - github.com/sergi/go-diff v1.0.0 // indirect - github.com/ugorji/go/codec v1.2.7 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasthttp v1.34.0 // indirect - github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect - github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect - github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect - github.com/yudai/gojsondiff v1.0.0 // indirect - github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect - golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect - golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect - golang.org/x/text v0.3.7 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect - moul.io/http2curl/v2 v2.3.0 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index f953d3c4..00000000 --- a/go.sum +++ /dev/null @@ -1,215 +0,0 @@ -github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= -github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.2/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fasthttp/websocket v1.4.3-rc.6 h1:omHqsl8j+KXpmzRjF8bmzOSYJ8GnS0E3efi1wYT+niY= -github.com/fasthttp/websocket v1.4.3-rc.6/go.mod h1:43W9OM2T8FeXpCWMsBd9Cb7nE2CACNqNvCqQCoty/Lc= -github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= -github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/gavv/httpexpect/v2 v2.8.0 h1:sIYO3vVjWq06X9LVncVXGvDGtVytedGLoJLp7tR+m5A= -github.com/gavv/httpexpect/v2 v2.8.0/go.mod h1:jIj2f4rLediVaQK7rIH2EcU4W1ovjeSI8D0g85VJe9o= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs= -github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= -github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw= -github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= -github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= -github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= -github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= -github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= -github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= -github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= -github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873 h1:N3Af8f13ooDKcIhsmFT7Z05CStZWu4C7Md0uDEy4q6o= -github.com/savsgio/gotils v0.0.0-20210617111740-97865ed5a873/go.mod h1:dmPawKuiAeG/aFYVs2i+Dyosoo7FNcm+Pi8iK6ZUrX8= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.27.0/go.mod h1:cmWIqlu99AO/RKcp1HWaViTqc57FswJOfYYdPJBl8BA= -github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= -github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= -github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= -github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= -github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= -github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= -github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= -github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= -github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= -github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= -golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -moul.io/http2curl/v2 v2.3.0 h1:9r3JfDzWPcbIklMOs2TnIFzDYvfAZvjeavG6EzP7jYs= -moul.io/http2curl/v2 v2.3.0/go.mod h1:RW4hyBjTWSYDOxapodpNEtX0g5Eb16sxklBqmd2RHcE= diff --git a/main.go b/main.go deleted file mode 100644 index 7eb95018..00000000 --- a/main.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -import ( - "github.com/RaymondCode/simple-demo/service" - "github.com/gin-gonic/gin" -) - -func main() { - go service.RunMessageServer() - - r := gin.Default() - - initRouter(r) - - r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080") -} diff --git a/public/bear.mp4 b/public/bear.mp4 deleted file mode 100644 index 3b468401..00000000 Binary files a/public/bear.mp4 and /dev/null differ