diff --git a/public/api/i/2025/07/19/uje4vo-1.webp b/public/api/i/2025/07/19/uje4vo-1.webp new file mode 100644 index 0000000..029bcb2 Binary files /dev/null and b/public/api/i/2025/07/19/uje4vo-1.webp differ diff --git a/src/content/posts/Golang/Gin框架快速入门.md b/src/content/posts/Golang/Gin框架快速入门.md new file mode 100644 index 0000000..c53c96a --- /dev/null +++ b/src/content/posts/Golang/Gin框架快速入门.md @@ -0,0 +1,1969 @@ +--- +title: Gin框架快速入门 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [Gin, Go, 框架, 快速入门] +category: 'Go' +draft: false +lang: '' +--- + +# Go 语言 Web 框架 Gin + +> 参考 +> +> +> + +# 返回各种值 + +## 返回字符串 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + c.String(http.StatusOK, "helloworld") + + }) + + router.Run(":8080") + +} +``` + +## 返回 json + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +type Student struct { + + Name string `json:"name"` + + Age int `json:"age"` + + Number string `json: "number"` + +} + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + var student Student = Student{ + + Name: "meowrain", + + Age: 20, + + Number: "10086", + + } + + c.JSON(http.StatusOK, student) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vs14ff-3.webp) + +## 返回 map + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + userMap := map[string]any{ + + "username": "meowrain", + + "age": 20, + + "number": 10086, + + } + + c.JSON(http.StatusOK, userMap) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vu58ct-3.webp) + +## 返回原始 json + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func main() { + + router := gin.Default() + + router.GET("/", func(c *gin.Context) { + + + + c.JSON(http.StatusOK, gin.H{ + + "username": "meowrain", + + + + "age": 20, + + + + "number": 10086, + + }) + + }) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/vuxhez-3.webp) + +> ![](https://blog.meowrain.cn/api/i/2024/03/07/vv144f-3.webp) + +## 返回 html 并传递参数 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.GET("/", _html) + + router.Run(":8080") + +} +``` + +```html + + + + + + + + + Document + + + +

User Information

+ +

Username: {{.obj.Username}}

+ +

Age: {{.obj.Age}}

+ + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10gipeq-3.webp) + +### 静态文件配置 + +`router.Static`和`router.StaticFS`都是用于处理静态文件的 Gin 框架路由处理方法,但它们有一些区别。 + +1. **`router.Static`**: + + - 使用 `router.Static` 时,Gin 会简单地将请求的 URL 路径与提供的本地文件系统路径进行映射。通常,这适用于将 URL 路径直接映射到一个静态文件或目录。 + - 示例:`router.Static("/static", "./static")` 将 `/static` 映射到当前工作目录下的 `./static` 文件夹。 + +2. **`router.StaticFS`**: + - `router.StaticFS` 则允许你使用 `http.FileSystem` 对象,这可以提供更多的灵活性。你可以使用 `http.Dir` 创建 `http.FileSystem`,并将其传递给 `router.StaticFS`。 + - 这允许你更灵活地处理静态文件,例如从不同的源(内存、数据库等)加载静态文件,而不仅限于本地文件系统。 + - 示例:`router.StaticFS("/static", http.Dir("/path/to/static/files"))` 使用本地文件系统路径创建一个 `http.FileSystem` 对象,然后将 `/static` 映射到这个文件系统。 + +总体而言,`router.Static`更简单,适用于基本的静态文件服务,而`router.StaticFS`提供了更多的灵活性,允许你自定义静态文件的加载方式。选择使用哪一个取决于你的具体需求。 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static/", "./static") + + router.GET("/", _html) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10q03tz-3.webp) + +```html + + + + + + + + + Document + + + +

User Information

+ +

Username: {{.obj.Username}}

+ +

Age: {{.obj.Age}}

+ + + + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10qf6z6-3.webp) + +# 重定向 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _html(c *gin.Context) { + + type UserInfo struct { + + Username string `json:"username"` + + Age int `json:"age"` + + Password string `json:"-"` + + } + + user := UserInfo{ + + Username: "meowrain", + + Age: 20, + + Password: "12345678", + + } + + c.HTML(http.StatusOK, "index.html", gin.H{"obj": user}) + +} + +func _redirect(c *gin.Context) { + + c.Redirect(301, "https://www.baidu.com") + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static/", "./static") + + router.GET("/", _html) + + router.GET("/baidu", _redirect) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/10xi5tr-3.webp) + +### 301 和 302 的区别 + +HTTP 状态码中的 301 和 302 分别表示重定向(Redirect)。它们之间的主要区别在于重定向的性质和原因: + +1. **301 Moved Permanently(永久重定向)**: + + - 当服务器返回状态码 301 时,它告诉客户端请求的资源已经被永久移动到新的位置。 + - 客户端收到 301 响应后,应该更新书签、链接等,将这个新的位置作为将来所有对该资源的请求的目标。 + - 搜索引擎在遇到 301 时,通常会更新索引,将原始 URL 替换为新的 URL。 + +2. **302 Found(临时重定向)**: + - 当服务器返回状态码 302 时,它表示请求的资源暂时被移动到了另一个位置。 + - 客户端收到 302 响应后,可以在不更新书签和链接的情况下继续使用原始 URL。 + - 搜索引擎在遇到 302 时,通常会保留原始 URL 在索引中,并不会立即更新为新的 URL。 + +总体来说,使用 301 通常是在确定资源永久移动的情况下,而 302 通常用于暂时性的重定向,即资源可能在将来回到原始位置。选择使用哪种状态码取决于你希望客户端和搜索引擎如何处理被重定向的资源。 + +# 路由 + +## 默认路由 + +> 当访问路径不被匹配的时候返回默认路由内容 + +目录结构 + +![image-20240308205926484](https://blog.meowrain.cn/api/i/2024/03/08/y21i4t-3.webp) + +```go +//main.go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.String(200, "helloworld") + }) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +//server.go +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} + +``` + +```html + + + + + + 404 NOT FOUND + + +

404 Not Found

+ + +``` + +> 效果 + +![](https://blog.meowrain.cn/api/i/2024/03/08/y25trv-3.webp) + +![image-20240308210004320](https://blog.meowrain.cn/api/i/2024/03/08/yqb64w-3.webp) + +## 路由组 + +> 参考: + +我们可以将拥有共同 URL 前缀的路由划分为一个路由组。习惯性一对`{}`包裹同组的路由,这只是为了看着清晰,你用不用`{}`包裹功能上没什么区别。 + +![](https://blog.meowrain.cn/api/i/2024/03/08/z7tbui-3.webp) + +```go +//main.go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + userGroup := router.Group("/user") + { + userGroup.GET("/all", controller.GetUserList) + userGroup.GET("/detail", controller.GetUserDetail) + } + router.LoadHTMLGlob("templates/*") + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +//controller/userController.go +package controller + +import ( + . "awesomeProject/pkg/entity" + "github.com/gin-gonic/gin" + "net/http" + "strconv" +) + +func GetUserList(c *gin.Context) { + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: UserList, + Msg: "返回成功", + }) +} +func GetUserDetail(c *gin.Context) { + id := c.Query("id") + for _, res := range UserList { + if strconv.Itoa(res.ID) == id { + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: res, + Msg: "get successfully", + }) + } + } +} + +``` + +```go +//user.go +package entity + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Age int `json:"age"` +} + +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +var UserList []User = []User{ + { + ID: 1, + Name: "meowrian", + Age: 20, + }, + { + ID: 2, + Name: "Mike", + Age: 30, + }, + { + ID: 3, + Name: "Amy", + Age: 23, + }, + { + ID: 4, + Name: "John", + Age: 24, + }, +} + + +``` + +```go +//server.go +package controller + +import ( + "github.com/gin-gonic/gin" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} + +``` + +![image-20240308213055236](https://blog.meowrain.cn/api/i/2024/03/08/z8h8cb-3.webp) + +![image-20240308213116279](https://blog.meowrain.cn/api/i/2024/03/08/z8u72c-3.webp) + +> 路由组也是支持嵌套的 + +# 参数 + +## 查询参数 + +```go +package main + + + +import ( + + "net/http" + + + + "github.com/gin-gonic/gin" + +) + + + +func _query(c *gin.Context) { + + user := c.Query("user") + + c.HTML(http.StatusOK, "index.html", gin.H{ + + "user": user, + + }) + +} + +func main() { + + router := gin.Default() + + router.LoadHTMLGlob("template/*") + + router.Static("/static", "./static") + + router.GET("/", _query) + + router.Run(":8080") + +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/112i3vb-3.webp) + +```go +package main + +import ( + "fmt" + "net/http" + "github.com/gin-gonic/gin") + +func _query(c *gin.Context) { + user, ok := c.GetQuery("user") + ids := c.QueryArray("id") //拿到多个相同的查询参数 + maps := c.QueryMap("id") + fmt.Println(maps) + if ok { + c.HTML(http.StatusOK, "index.html", gin.H{ + "user": user, + "id": ids, + }) + } else { + c.String(http.StatusOK, "No query!") + } +} + +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", _query) + router.Run(":8080") +} +``` + +> 请求为: > ![](https://blog.meowrain.cn/api/i/2024/03/07/12532gz-3.webp) > ![](https://blog.meowrain.cn/api/i/2024/03/07/1267bmh-3.webp) + +## 动态参数 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +func _param(c *gin.Context) { + param := c.Param("user_id") + fmt.Println(param) + c.HTML(http.StatusOK, "index.html", gin.H{ + "param": param, + }) + +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/param/:user_id", _param) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12ac8nv-3.webp) + +## 表单参数 PostForm + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +func postForm(c *gin.Context) { + name := c.PostForm("name") + password := c.PostForm("password") + c.JSON(http.StatusOK, gin.H{ + "name": name, + "password": password, + }) +} +func index(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", index) + router.POST("/post", postForm) + router.Run(":8080") +} +``` + +```html + + + + + + Post Form Test + + +

Post Form Test

+
+ + +
+ + +
+ +
+

Response:

+ + + +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12lcebn-3.webp) + +postFormArray 函数 + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +func postForm(c *gin.Context) { + name := c.PostForm("name") + password := c.PostForm("password") + respArr := c.PostFormArray("name") + c.JSON(http.StatusOK, gin.H{ + "name": name, + "password": password, + "respArray": respArr, + }) +} +func index(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{}) +} +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", index) + router.POST("/post", postForm) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/07/12n0072-3.webp) + +## 原始参数 + +```go +/* +原始参数 +*/ +package main + +import ( + "fmt" + "github.com/gin-gonic/gin") + +func _raw(c *gin.Context) { + buf, err := c.GetRawData() + if err != nil { + fmt.Println("error:", err) + return + } + fmt.Println(string(buf)) +} +func main() { + router := gin.Default() + router.POST("/", _raw) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/extkqj-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/exvpa7-3.webp) + +### 解析 json 数据 + +```go +/* +原始参数 +*/ +package main + +import ( + "encoding/json" + "fmt" "github.com/gin-gonic/gin") + +func bindJSON(c *gin.Context, obj any) error { + body, err := c.GetRawData() + contentType := c.GetHeader("Content-Type") + fmt.Println("ContentType:", contentType) + if err != nil { + fmt.Println("error:", err) + return err + } + switch contentType { + case "application/json": + err := json.Unmarshal(body, obj) + if err != nil { + fmt.Println(err.Error()) + return err + } + } + return nil +} + +func raw(c *gin.Context) { + type User struct { + Name string `json:"name"` + Age int `json:"age"` + Password string `json:"-"` + } + var user User + err := bindJSON(c, &user) + if err != nil { + fmt.Println("Error binding JSON:", err) + return + } + fmt.Println(user) +} + +func main() { + router := gin.Default() + router.POST("/", raw) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/fkacjn-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/fki60h-3.webp) + +# 四大请求方式 + +![](https://blog.meowrain.cn/api/i/2024/03/08/fnulpk-3.webp) + +## 简单实现以下 CRUD + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http" "strconv") + +type Article struct { + Id int `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + Author string `json:"author"` +} + +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +var articleList []Article = []Article{ + { + 1, + "Go语言从入门到精通", + "Learn better", + "Mike Jason", + }, + { + 2, + "Java从入门到精通", + "Java is good", + "Jack Smith", + }, + { + 3, + "Javascript从入门到精通", + "Javascript is a nice programming language!", + "Amy Gorden", + }, + { + 4, + "Python从入门到精通", + "Python is a simple language!", + "Jack Buffer", + }, +} + +/*简单增删改查*/ +func _getList(c *gin.Context) { + + c.JSON(http.StatusOK, Response{Code: 200, Data: articleList, Msg: "获取成功"}) +} +func _getDetail(c *gin.Context) { + id := c.Param("id") + flag := false + for _, res := range articleList { + if strconv.Itoa(res.Id) == id { + flag = true + c.JSON(http.StatusOK, Response{ + Code: 200, + Data: res, + Msg: "获取成功!", + }) + } + } + if flag == false { + c.JSON(404, Response{ + Code: 404, + Data: "Not Found the data", + Msg: "获取失败,因为数据不存在", + }) + } +} +func _create(c *gin.Context) { + id, _ := strconv.ParseInt(c.PostForm("id"), 10, 0) + title := c.PostForm("title") + content := c.PostForm("content") + author := c.PostForm("author") + var article Article = Article{ + Id: int(id), + Title: title, + Content: content, + Author: author, + } + articleList = append(articleList, article) + c.JSON(200, Response{Code: 200, Data: article, Msg: "添加成功!"}) +} +func _delete(c *gin.Context) { + id := c.Param("id") + index := -1 + for i, res := range articleList { + if strconv.Itoa(res.Id) == id { + index = i + break + } + } + if index != -1 { + articleList = append(articleList[:index], articleList[index+1:]...) + c.JSON(http.StatusOK, Response{Code: 200, Data: nil, Msg: "删除成功"}) + } else { + c.JSON(http.StatusNotFound, Response{Code: 404, Data: "Not Found the data", Msg: "删除失败,数据不存在"}) + } +} +func _update(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + title := c.PostForm("title") + content := c.PostForm("content") + author := c.PostForm("author") + found := false + for i, res := range articleList { + if res.Id == id { + found = true + articleList[i] = Article{ + id, + title, + content, + author, + } + break + } + } + if found { + c.JSON(http.StatusOK, Response{ + Code: 200, + Data: nil, + Msg: "更新成功", + }) + return + } else { + c.JSON(http.StatusNotFound, Response{ + Code: 404, + Data: "Not found the data", + Msg: "更新失败,因为数据不存在", + }) + } + +} + +func main() { + router := gin.Default() + router.GET("/articles", _getList) + router.GET("/articles/:id", _getDetail) + router.POST("/articles", _create) + router.PUT("/articles/:id", _update) + router.DELETE("/articles/:id", _delete) + router.Run(":8080") + +} +``` + +## 文件上传 + +### 上传单个文件 + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + router.POST("/upload",controller.Upload_file) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +//文件上传 +func Upload_file(c *gin.Context) { + file, err := c.FormFile("f1") + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + log.Println(file.Filename) + dst := fmt.Sprintf("./tmp/%s", file.Filename) + c.SaveUploadedFile(file, dst) + c.JSON(http.StatusOK, gin.H{ + "message": fmt.Sprintf("'%s' uploaded", file.Filename), + }) +} + +``` + +```html + + + + 上传文件示例 + + + +
+ + +
+ + + + +``` + +![image-20240308214643811](https://blog.meowrain.cn/api/i/2024/03/08/zhxp6e-3.webp) + +![image-20240308220222511](https://blog.meowrain.cn/api/i/2024/03/08/10f5j2o-3.webp) + +### 上传多个文件 + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + router.LoadHTMLGlob("templates/*") + router.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + router.POST("/upload", controller.UploadFiles) + router.NoRoute(controller.Default_route) + router.Run(":80") +} + +``` + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +func UploadFiles(c *gin.Context) { + err := c.Request.ParseMultipartForm(100 << 20) // 100 MB limit + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + form := c.Request.MultipartForm + if form == nil || form.File == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "No files provided in the request", + }) + return + } + + files := form.File["f1"] + + for _, file := range files { + dst := fmt.Sprintf("./tmp/%s", file.Filename) + if err := c.SaveUploadedFile(file, dst); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": fmt.Sprintf("Failed to save file %s: %s", file.Filename, err.Error()), + }) + return + } + log.Println(file.Filename) + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Files uploaded successfully", + }) +} + +``` + +```html + + + + 上传文件示例 + + + +
+ + +
+ + + + +``` + +![image-20240308220131880](https://blog.meowrain.cn/api/i/2024/03/08/10em1sq-3.webp) + +![](https://blog.meowrain.cn/api/i/2024/03/08/10enova-3.webp) + +![image-20240308220155287](https://blog.meowrain.cn/api/i/2024/03/08/10er6r4-3.webp) + +### 判断上传文件的类型 + +在 Gin 框架中,可以使用`binding`模块提供的`FormFile`函数来获取上传的文件,然后检查文件的 MIME 类型。具体步骤如下: + +1. 在处理函数中使用`c.FormFile`获取上传的文件: + +```go +file, err := c.FormFile("file") +if err != nil { + c.String(http.StatusBadRequest, "获取文件失败") + return +} +``` + +2. 打开文件并读取文件头部的几个字节,以识别文件的 MIME 类型: + +```go +f, err := file.Open() +if err != nil { + c.String(http.StatusInternalServerError, "打开文件失败") + return +} +defer f.Close() + +buffer := make([]byte, 512) +_, err = f.Read(buffer) +if err != nil { + c.String(http.StatusInternalServerError, "读取文件失败") + return +} +``` + +3. 使用`http.DetectContentType`函数检测文件的 MIME 类型: + +```go +contentType := http.DetectContentType(buffer) +``` + +4. 判断文件类型是否允许: + +```go +allowedTypes := []string{"image/jpeg", "image/png", "application/pdf"} +allowed := false +for _, t := range allowedTypes { + if t == contentType { + allowed = true + break + } +} + +if !allowed { + c.String(http.StatusBadRequest, "不支持的文件类型") + return +} +``` + +完整的示例代码如下: + +```go +func uploadFile(c *gin.Context) { + file, err := c.FormFile("file") + if err != nil { + c.String(http.StatusBadRequest, "获取文件失败") + return + } + + f, err := file.Open() + if err != nil { + c.String(http.StatusInternalServerError, "打开文件失败") + return + } + defer f.Close() + + buffer := make([]byte, 512) + _, err = f.Read(buffer) + if err != nil { + c.String(http.StatusInternalServerError, "读取文件失败") + return + } + + contentType := http.DetectContentType(buffer) + allowedTypes := []string{"image/jpeg", "image/png", "application/pdf"} + allowed := false + for _, t := range allowedTypes { + if t == contentType { + allowed = true + break + } + } + + if !allowed { + c.String(http.StatusBadRequest, "不支持的文件类型") + return + } + + // 处理文件... +} +``` + +在上面的示例中,我们定义了一个允许的 MIME 类型列表`allowedTypes`,包括`image/jpeg`、`image/png`和`application/pdf`。如果上传的文件类型不在允许列表中,就会返回错误响应。你可以根据需求修改允许的文件类型列表。 + +### 使用 gin 编写文件服务器 + +```go +package controller + +import ( + "fmt" + "github.com/gin-gonic/gin" + "log" + "net/http" + "os" +) + +func Default_route(c *gin.Context) { + c.HTML(http.StatusNotFound, "404.html", nil) +} +func UploadFiles(c *gin.Context) { + err := c.Request.ParseMultipartForm(100 << 20) // 100 MB limit + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": err.Error(), + }) + return + } + + form := c.Request.MultipartForm + if form == nil || form.File == nil { + c.JSON(http.StatusBadRequest, gin.H{ + "message": "No files provided in the request", + }) + return + } + + files := form.File["f1"] + + for _, file := range files { + dst := fmt.Sprintf("./tmp/%s", file.Filename) + if err := c.SaveUploadedFile(file, dst); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{ + "message": fmt.Sprintf("Failed to save file %s: %s", file.Filename, err.Error()), + }) + return + } + log.Println(file.Filename) + } + + c.JSON(http.StatusOK, gin.H{ + "message": "Files uploaded successfully", + }) +} +func ListFiles(c *gin.Context) { + // 读取 ./tmp 目录下的所有文件 + files, err := os.ReadDir("./tmp") + if err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + + // 渲染模板 + c.HTML(http.StatusOK, "download.html", gin.H{ + "Files": files, + }) +} + +``` + +```go +package main + +import ( + "awesomeProject/pkg/controller" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // 设置静态文件路径为 ./tmp + r.Static("/tmp", "./tmp") + + // 设置模板目录 + r.LoadHTMLGlob("templates/*") + + // 定义路由 + r.GET("/", func(c *gin.Context) { + c.HTML(200, "upload.html", nil) + }) + r.POST("/upload", controller.UploadFiles) + + //文件列表服务器 + r.GET("/files", controller.ListFiles) + + // 启动HTTP服务器 + r.Run(":8080") +} + +``` + +```html + + + + + File List + + +

File List

+ + + +``` + +```html + + + + + File List + + + +

File List

+ + + +``` + +![image-20240308222026615](https://blog.meowrain.cn/api/i/2024/03/08/10pw52c-3.webp) + +![image-20240308222225060](https://blog.meowrain.cn/api/i/2024/03/08/10r2oqf-3.webp) + +![image-20240308222527025](https://blog.meowrain.cn/api/i/2024/03/08/10svloq-3.webp) + +# 请求头相关 + +## 获取所有请求头 + +![](https://blog.meowrain.cn/api/i/2024/03/08/iu6r5m-3.webp) + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +func main() { + router := gin.Default() + router.LoadHTMLGlob("template/*") + router.Static("static", "./static") + router.GET("/", func(c *gin.Context) { + c.HTML(http.StatusOK, "index.html", gin.H{ + "header": c.Request.Header, + }) + fmt.Println(c.Request.Header) + }) + router.Run(":8080") +} +``` + +```html + + + + + + Post Form Test + + +

Header Test

+

Header: {{.header}}

+ + +``` + +## 绑定参数 bind + +> 绑定 post 发送的 json 数据转换为 Student 结构体的成员变量值,然后再把这个结构体转换为 json 对象 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name"` + Age int `json:"age"` +} + +func main() { + router := gin.Default() + router.POST("/", func(c *gin.Context) { + var stu Student + err := c.BindJSON(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/shcott-3.webp) + +> 绑定查询参数 + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name" form:"name"` + Age int `json:"age" form:"age"` +} + +func main() { + router := gin.Default() + router.GET("/", func(c *gin.Context) { + var stu Student + err := c.BindQuery(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/sjnhl2-3.webp) + +> bind URI + +```go +package main + +import ( + "fmt" + "github.com/gin-gonic/gin" "net/http") + +type Student struct { + Name string `json:"name" form:"name" uri:"name"` + Age int `json:"age" form:"age" uri:"age"` +} + +func main() { + router := gin.Default() + router.GET("/uri/:name/:age", func(c *gin.Context) { + var stu Student + err := c.ShouldBindUri(&stu) + if err != nil { + fmt.Println("error: ", err) + c.JSON(http.StatusBadGateway, err) + return + } + c.JSON(http.StatusOK, stu) + }) + router.Run(":8080") +} +``` + +![](https://blog.meowrain.cn/api/i/2024/03/08/sm69pi-3.webp) + +## 常用验证器 + +``` +// 不能为空,并且不能没有这个字段 +required: 必填字段,如:binding:"required" + +// 针对字符串的长度 +min 最小长度,如:binding:"min=5" +max 最大长度,如:binding:"max=10" +len 长度,如:binding:"len=6" + +// 针对数字的大小 +eq 等于,如:binding:"eq=3" +ne 不等于,如:binding:"ne=12" +gt 大于,如:binding:"gt=10" +gte 大于等于,如:binding:"gte=10" +lt 小于,如:binding:"lt=10" +lte 小于等于,如:binding:"lte=10" + +// 针对同级字段的 +eqfield 等于其他字段的值,如:PassWord string `binding:"eqfield=Password"` +nefield 不等于其他字段的值 + + +- 忽略字段,如:binding:"-" +``` + +```go +package main + +import ( + "github.com/gin-gonic/gin" + "net/http") + +type User struct { + Name string `json:"name" binding:"required"` + Password string `json:"password" binding:"eqfield=Re_Password"` + Re_Password string `json:"re_password"` +} +type Response struct { + Code int `json:"code"` + Data any `json:"data"` + Msg string `json:"msg"` +} + +func main() { + router := gin.Default() + router.POST("/login", func(c *gin.Context) { + var user User + err := c.ShouldBindJSON(&user) + if err != nil { + c.JSON(http.StatusBadGateway, Response{ + Code: http.StatusBadGateway, + Data: err.Error(), + Msg: "bad response", + }) + return + } + c.JSON(http.StatusOK, Response{ + Code: http.StatusOK, + Data: user, + Msg: "post successfully", + }) + }) + router.Run(":8080") +} +``` + +> 密码相同 +> ![](https://blog.meowrain.cn/api/i/2024/03/08/t2zolw-3.webp) + +> 密码不同 +> ![](https://blog.meowrain.cn/api/i/2024/03/08/t3ebk2-3.webp) + +> 我们看到报错对用户不是很友好,我们可以自定义验证的错误信息 +> +> TODO + +## [gin 内置验证器](https://docs.fengfengzhidao.com/#/docs/Gin%E6%A1%86%E6%9E%B6%E6%96%87%E6%A1%A3/4.bind%E7%BB%91%E5%AE%9A%E5%99%A8?id=gin%e5%86%85%e7%bd%ae%e9%aa%8c%e8%af%81%e5%99%a8) + +``` +// 枚举 只能是red 或green +oneof=red green + +// 字符串 +contains=fengfeng // 包含fengfeng的字符串 +excludes // 不包含 +startswith // 字符串前缀 +endswith // 字符串后缀 + +// 数组 +dive // dive后面的验证就是针对数组中的每一个元素 + +// 网络验证 +ip +ipv4 +ipv6 +uri +url +// uri 在于I(Identifier)是统一资源标示符,可以唯一标识一个资源。 +// url 在于Locater,是统一资源定位符,提供找到该资源的确切路径 + +// 日期验证 1月2号下午3点4分5秒在2006年 +datetime=2006-01-02 +``` + +--- + +# Gin 中间件 + +> + +Gin 中的中间件必须是一个`gin.HandlerFunc`类型。 + +Gin 框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。 diff --git a/src/content/posts/Golang/Go_map底层结构.md b/src/content/posts/Golang/Go_map底层结构.md new file mode 100644 index 0000000..bb22ffc --- /dev/null +++ b/src/content/posts/Golang/Go_map底层结构.md @@ -0,0 +1,326 @@ +--- +title: Go_map底层结构 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [切片, Golang, Go] +category: 'Go' +draft: false +lang: '' +--- +# Golang map底层数据结构 + + + +[Golang map 实现原理](https://mp.weixin.qq.com/s?__biz=MzkxMjQzMjA0OQ==&mid=2247483868&idx=1&sn=6e954af8e5e98ec0a9d9fc5c8ceb9072&chksm=c10c4f02f67bc614ff40a152a848508aa1631008eb5a600006c7552915d187179c08d4adf8d7&scene=0&xtrack=1&subscene=90#rd) + +## 概述 + +map是一种常用的数据结构,核心特征包括下面三点: + +- 存储基于key-value对映射的模式 +- 基于key维度实现存储数据的去重 +- 读,写,删操作控制,时间复杂度O(1) + +![image-20250402212335440](https://blog.meowrain.cn/api/i/2025/04/02/n5y1Lh1743600215837401704.avif) + +### 初始化方法 + +```go +map1 := make(map[string]int) + +map2 := map[string]int{ + "m1": 1, + "m2":2, +} + +``` + +### key 类型要求 + +map中,key的数据类型必须是可以比较的类型,slice,chan,func,map不可比较,所以不能作为map的key + +![image-20250402210528197](https://blog.meowrain.cn/api/i/2025/04/02/fFJnr51743599129052146367.avif) + +![image-20250402210536019](https://blog.meowrain.cn/api/i/2025/04/02/3eTMZz1743599137424628575.avif) + +![image-20250402210601926](https://blog.meowrain.cn/api/i/2025/04/02/yP4UYM1743599162191602503.avif) + +![image-20250402210607311](https://blog.meowrain.cn/api/i/2025/04/02/KZhyJp1743599167648587617.avif) + +![image-20250402210620988](https://blog.meowrain.cn/api/i/2025/04/02/QFpAk21743599181398433044.avif) + +# 核心原理 + +map又称为hash map,算法上基于hash实现key的映射和寻址,在数据结构上基于桶数组实现key-value对的存储 + +以一组key-value对写入map的流程进行简述: + +1. 通过哈希方法去的key的hash值‘ +2. hash值对同数组长度取模,确定它所属的桶 +3. 在桶中插入key value对 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/MmAiV11743599321939050023.avif) + +## hash + +hash 译作散列,是一种将任意长度的输入压缩到某一固定长度的输出摘要的过程,由于这种转换属于压缩映射,输入空间远大于输出空间,因此不同输入可能会映射成相同的输出结果. 此外,hash在压缩过程中会存在部分信息的遗失,因此这种映射关系具有不可逆的特质. + +1. hash的可重入性: 相同的key,必然产生相同的hash值 +2. hash的离散性: 只要两个key不相同,不论他们相似度的高低,产生的hash值会在整个输出域内均匀地离散化 +3. hash的单向性: 企图通过hash值反向映射会key是无迹可寻的。 +4. hash冲突: 由于输入域无穷大,输出域有限,必然存在不同key映射到相同hash值的情况,这种情况叫做哈希冲突 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/RV0Syj1743599459574600284.avif) + +## 桶数组 + +map中,会通过长度为2的整数次幂的桶数组进行key-value对的存储 + +1. 每个桶固定可以存放8个key-value对 +2. 倘若超过8个key-value对打到桶数组的同一个索引当中,此时会通过创建桶链表的方式来化解这个问题。 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/X7NMOa1743599952016346994.avif) + +## 拉链法解决hash冲突 + +首先,由于hash冲突的存在,不同的key可能存在相同的hash值 + +再者,hash值会对桶数组长度取模,因此不同的hash值可能被打到同一个桶中 + +综上,不同的key-value可能被映射到map的同一个桶当中。 + +拉链法中,将命中同一个桶的元素通过链表的形式进行连接,因此便于动态扩展 + +> 只有当一个桶已经满了(8 个 kv 对),并且又有新的 key 哈希到这个桶时,才会创建溢出桶,并将新的 key-value 对存储到溢出桶中,然后将该溢出桶链接到原桶的尾部。 后续再有冲突的 kv 对,也会被添加到溢出桶或者新的溢出桶中,形成一个链表。 + +![img](https://blog.meowrain.cn/api/i/2025/04/02/lgobAo1743600543664079674.avif) + +## 开放寻址法解决hash冲突 + +> 开放寻址法是一种解决哈希冲突的方法,它在哈希表中寻找另一个空闲位置存储冲突的元素,也就是说,所有元素都直接存储在哈希表的桶中 +> +> 开放寻址法是一种在哈希表中解决冲突的方法。当两个不同的键映射到同一个索引位置时,就会发生冲突。开放寻址法不是使用链表等额外的数据结构来存储冲突的键值对,而是尝试在哈希表本身中寻找一个空闲的位置来存储新的键值对。 + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/GNSRsu1743600902616857141.avif) + +常见开放寻址技术: + +- 线性寻址: 如果在索引`i`发生冲突,线性探测会依次检查`i+1`,`i+2`,`i+3`等位置,直到找到一个空闲的槽位 +- 二次探测检查 `i + 1^2`、`i + 2^2`、`i + 3^2` 等位置。与线性探测相比,这有助于减少聚集现象。 +- 双重哈希: 双重哈希使用第二个哈希函数来确定探测的步长。如果第一个哈希函数在索引`i`导致哈希冲突,第二个哈希函数hash2(key)用于确定探测的间隔(例如,`i + hash2(key)`、`i + 2*hash2(key)`、`i + 3*hash2(key)` 等)。 + +![image-20250402213515236](https://blog.meowrain.cn/api/i/2025/04/02/lsNJNR1743600915626536212.avif) + +我们的golang map解决哈希冲突的方式结合了拉链法和开放寻址法。 + +- 桶: map的底层数据结构是一个桶数组,每个桶严格意义上是一个单向桶链表 +- 桶的大小: 每个桶可以固定存放8个key value对 +- 当key命中一个桶的时候,首先根据开放寻址法,在桶的8个位置中寻找空位进行插入 +- 倘若8个位置都已经被占满,就基于桶的溢出桶指针,找到下一个桶(重复第三步) +- 倘若遍历到链表尾部,还没找到空位,就用拉链法,在桶链表尾部接入新桶,并且插入key-value对 + +![image-20250402215431186](https://blog.meowrain.cn/api/i/2025/04/02/PB9PuR1743602071901331051.avif) + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/Xlpg4R1743602154258822359.avif) + +## 扩容性能优化 + +倘若map的桶数组长度固定不变,那么随着key-value对数量的增长,当一个桶下挂载的key-value达到一定的量级,此时操作的时间复杂度会趋于线性,无法满足诉求。 + +**桶数组长度固定不变 + key-value 对数量持续增加 => 哈希冲突加剧 => Bucket 链表变长 => 查找/插入/删除 需要遍历长链表 => 操作时间复杂度接近 O(n) (线性)** + +因此在设计上,map桶的数组长度会随着key-value对的数量变化而实时调整。保证每个桶内的key-value对数量始终控制在常量级别。 + +扩容类型分为: + +- 增量扩容 +- 等量扩容 + +### 增量扩容 + +触发条件: `key-value总数 / 桶数组长度 > 6.5`的时候,发生增量扩容 + +扩容方式: 桶数组长度增长为原来的`两倍` + +目的: 减少负载因子,降低平均查找时间 + +负载因子: `key-value总数 / 桶的数量` + +![image-20250402225053461](https://blog.meowrain.cn/api/i/2025/04/02/exh1He1743605454120683710.avif) + +### 等量扩容 + +触发条件: 当桶内溢出桶数量大于等于2^B时(B 为桶数组长度的指数,B 最大取 15),发生等量扩容。) + +扩容方式: 桶的长度保持为原来的值 + +**目的:** 解决哈希冲突严重的问题,可能由于哈希函数选择不佳导致大量 key 映射到相同的桶,即使负载因子不高,也会出现大量溢出桶。 等量扩容旨在重新组织数据,减少溢出桶的数量。 + +![image-20250402231943679](https://blog.meowrain.cn/api/i/2025/04/02/m4tdlZ1743607184556640257.avif) + +![image-20250402231929805](https://blog.meowrain.cn/api/i/2025/04/02/7Rrm4l1743607170611676452.avif) + +### 渐进式扩容 + +![image-20250402233251365](https://blog.meowrain.cn/api/i/2025/04/02/8hpZdr1743607972891808021.avif) + +![图片](https://blog.meowrain.cn/api/i/2025/04/02/2Cb2MO1743608023551743628.avif) + +# 数据结构 + +## hmap + +```go +type hmap struct { + count int // map中键值对的数量 + flags uint8 // map的状态标志位,用来指示map的当前状态(正在写入,正在扩容等) + B uint8 // buckets 数组的对数大小,2^B 是buckets数组的长度,比如B是5,那么桶数组的长度就是2^5 = 32 + noverflow uint16 //溢出桶数量的近似值 用来判断是否需要扩容 + hash0 uint32 // 哈希种子 + buckets unsafe.Pointer //指向bucket数组的指针,数组大小为2 ^ B,如果count == 0,那么buckets可能为nil + oldbuckets unsafe.Pointer // 如果发生扩容,指向旧的buckets数组 + nevacuate uintptr // 扩容的时候,表示旧buckcet数组已经迁移到新bucket数组的数量计数器 + extra *mapextra // 可选字段,用来保存overflow buckets的信息 +} +``` + +flags: map状态标识,其包含的主要状态为(这里面牵扯到很多概念还没有涉及,可以先大致的了解一下各自的含义) + +- iterator(`0b0001`): 当前map可能正在被遍历 +- oldIterator(`0b0010`): 当前map的旧桶可能正在被遍历 +- hashWrting(`0b0100`): 一个goroutine正在向map中写入数据 +- sameSizeGrow(`0b1000`): 等量扩容标志字段 + +## bmap + +![](https://blog.meowrain.cn/api/i/2025/04/04/Nb8mWR1743757559555396698.avif) + +![](https://blog.meowrain.cn/api/i/2025/04/04/R3jihc1743757664615047610.avif) + +> bmap就是map中的桶,可以存储8组key-value对数据,以及一个只想下一个溢出桶的指针 + +![](https://blog.meowrain.cn/api/i/2025/04/04/cH27qX1743757980953367677.avif) + +每一组key-value对数据包含key高8位hash值tophash,key,value三部分 + +我们来看看bmap(桶)的内存模型 + +![](https://blog.meowrain.cn/api/i/2025/04/04/4iwDeb1743757807687319535.avif) + +如果按照 `key/value/key/value/...` 这样的模式存储,那在每一个 key/value 对之后都要额外 padding 7 个字节;而将所有的 key,value 分别绑定到一起,这种形式 `key/key/.../value/value/...`,则只需要在最后添加 padding。 + +每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个 bucket ,通过 `overflow` 指针连接起来。 + +### tophash的作用? + +是key 哈希值的高8位 + +tophash的核心作用是**判断一个键是否可能存在于当前桶中,从而优化查询效率。** + +## 溢出桶数据结构 mapextra + +在map初始化的时候会根据初始数据量不同,自动创建不同数量的溢出桶。在物理结构上初始的正常同和溢出桶是连续存放的,正常桶和溢出桶之间的关系是靠链表来维护的。 + +> `mapextra` 就是在扩容时提供了一批预备的 `bmap`,然后利用 `bmap.overflow` 把它们链接起来。 + +```go +type mapextra struct { + overflow *[]*bmap // overflow buckets 的指针数组 + oldoverflow *[]*bmap // 旧的 overflow buckets 的指针数组 + + nextOverflow *bmap // 指向空闲的 overflow bucket +} + +``` + +在map初始化的时候,倘若容量过大,会提前申请好一批溢出桶,供后续使用,这部分溢出桶存放在hmap.mapextra当中: + +mapextra.overflow 是一个指向溢出桶切片的指针,这个切片里面的溢出桶是当前使用的,用于存储hmap.buckets中的桶的溢出数据。 + +mapextra.oldoverflow 也是一个指向溢出桶切片的指针,但是它指向的是旧的桶数组的溢出桶。 + +nextOverflow指向下一个可用的溢出桶 + +![](https://blog.meowrain.cn/api/i/2025/04/04/eZLvxe1743757352736850834.avif) + +--- + +# 什么是哈希种子? + +哈希种子(hash seed)是一个随机生成的数值,被用作哈希函数的一部分,来增加哈希值的随机性和不可预测性,可以把它理解为哈希函数的“盐” + +# go map 如何根据key的哈希值确定键值存储到哪个桶中? + +## 哈希值的作用 + +- 首先,当你在 Go map 中插入一个键值对时,Go runtime 会对键进行哈希运算,生成一个哈希值(一个整数)。 优秀的哈希函数应该能够将不同的键尽可能均匀地映射到不同的哈希值,以减少哈希碰撞的概率。 +- 这个哈希值是确定键值对存储位置的关键。 + +## go map 数据结构中hmap 中B的作用 + +我们通过哈希值的低B位作为bucket数组的索引, 来选择键值该存储到哪个bucket中。 + +公式 `bucketIndex = hash & ((1 << B) - 1)` + +上面的公式 用来**保留 `hash` 的低 `B` 位,并将其他位设置为 0**。 + +![image-20250402234237409](https://blog.meowrain.cn/api/i/2025/04/02/Vtatge1743608558267235069.avif) + +# key定位过程 + +key经过哈希计算后得到哈希值,共64个bit位,计算它到底要落在哪个桶的时候,只会用到最后B个bit位(log2BucketCount) + +例如,现在有一个key经过哈希函数计算后,得到的哈希结果是: + +``` + 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010 +``` + +而我们的B是5,也就是有2^5 = 32个桶 + +取最后五位,也就是 **01010** 转换为10进制也就是10,也就是 **10号桶**,这个操作其实是 **取余操作**,但是取余数开销太大,就用上面的位运算代替了。 + +接下来我们再用 **hash值的高8位**找到key在 **10号桶**中的位置 **1001011转换为10进制也就是 75**.最开始桶内还没有 key,新加入的 key 会找到第一个空位,放入。 + +![](https://blog.meowrain.cn/api/i/2025/04/04/JcLsW91743759107613607466.avif) + +![](https://blog.meowrain.cn/api/i/2025/04/04/dCIofJ1743759450720106234.avif) + +# 流程 + +![](https://blog.meowrain.cn/api/i/2025/04/05/VUcqQy1743839544909227250.avif) + +# 写入流程 + +写入流程: + +- 进行hmap是否为nil的检查,如果为空,就触发panic +- 进行并发读写的检查,倘若已经设置了并发读写标记,就抛出"concurrent map writes"异常。 +- 处理桶迁移。如果正在扩容,把key所在的旧桶数据迁移到新桶,同时迁移index位h.nevacuate的桶,迁移完成后h.nevacuate自增。更新迁移进度。如果所有桶迁移完毕,清除正在扩容的标记。 +- 查找 key 所在的位置,并记录桶链表的第一个空闲位置(若此 key 之前不存在,则将该位置作为插入位置)。 +- 若此 key 在桶链表中不存在,判断是否需要扩容,若溢出桶过多,则进行相同容量的扩容,否则进行双倍容量的扩容。 +- 若桶链表没有空闲位置,则申请溢出桶来存放 key - value 对。 +- 设置 key 和 tophash[i] 的值。 +- 返回 value 的地址。 + +# 删除流程 + +删除流程: + +- 进行并发读写检查。 +- 处理桶迁移,如果map处于正在扩容的状态,就迁移两个桶 +- 定位key所在的位置 +- 删除kv对的占用,这里是伪删除,只有在下次扩容的时候,被删除的key所占用的同空间才会得到释放。 +- map首先会将对应位置的tophash[i]设置为emptyOne,表示该位置被删除 +- 如果tophash[i]后面还有有效的节点,就仅设置为emptyOne标志,意味着这个节点后面仍然存在有效的key-value对 ,后续在查找某个key的时候,这个节点只后仍然需要继续查找 +- 要是tophash[i]是桶链表的最后一个有效节点,那么从这个节点往前遍历,将链表最后面所有标志位emptyOne的位置,都设置为emptyRest。这样在查找某个key的时候,emptyRest之后的节点不需要继续查找。 + +> - **`emptyOne`:** 表示当前 cell 是空的,但**不能保证**后面的 cell 也是空的。 +> - **`emptyRest`:** 表示当前 cell 是空的,并且**保证**后面的所有 cell 也是空的,直到遇到一个非空 cell 或者到达桶的末尾。 + +# 迭代流程 + +在每次对 map 进行循环时,会调用 mapiterinit 函数,以确定迭代从哪个桶以及桶内的哪个位置起始。由于 mapiterinit 内部是通过随机数来决定起始位置的,所以 map 循环是无序的,每次循环所返回的 key - value 对的顺序都各不相同。 + +![](https://blog.meowrain.cn/api/i/2025/04/05/TABXTR1743840105513843585.avif) diff --git a/src/content/posts/Golang/Go_slice切片原理.md b/src/content/posts/Golang/Go_slice切片原理.md new file mode 100644 index 0000000..34fc001 --- /dev/null +++ b/src/content/posts/Golang/Go_slice切片原理.md @@ -0,0 +1,189 @@ +--- +title: Go_slice切片原理 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [切片, Golang, Go] +category: 'Go' +draft: false +lang: '' +--- +# slice数据结构 + +数据结构 +我们每定义一个slice变量,golang底层都会构建一个slice结构的对象。slice结构体由3个成员变量构成: + +array表示数组指针,数组用于存储数据。 +len表示切片长度,也就是数组index从0到len-1已存储数据。 +cap表示切片容量,当切片长度超过最大容量时,需要扩容申请更大长度的数组。 + +```go +type slice struct { + array unsafe.Pointer // 数组指针 + len int // 切片长度 + cap int // 切片容量 +} +``` + +# 扩容原理 + +切片的扩容流程源码位于 runtime/slice.go 文件的 growslice 方法当中,其中核心步骤如下: + +• 倘若扩容后预期的新容量小于原切片的容量,则 panic + +• 倘若切片元素大小为 0(元素类型为 struct{}),则直接复用一个全局的 zerobase 实例,直接返回 + +• 倘若预期的新容量超过老容量的两倍,则直接采用预期的新容量 + +• 倘若老容量小于 256,则直接采用老容量的2倍作为新容量 + +• 倘若老容量已经大于等于 256,则在老容量的基础上扩容 1/4 的比例并且累加上 192 的数值,持续这样处理,直到得到的新容量已经大于等于预期的新容量为止 + +• 结合 mallocgc 流程中,对内存分配单元 mspan 的等级制度,推算得到实际需要申请的内存空间大小 + +• 调用 mallocgc,对新切片进行内存初始化 + +• 调用 memmove 方法,将老切片中的内容拷贝到新切片中 + +• 返回扩容后的新切片 + +```go +// nextslicecap computes the next appropriate slice length. +func nextslicecap(newLen, oldCap int) int { + newcap := oldCap // 将新容量初始化为旧容量 + doublecap := newcap + newcap // 计算旧容量的两倍 + + // 如果所需的新长度大于旧容量的两倍,则直接使用所需的新长度 + if newLen > doublecap { + return newLen + } + + const threshold = 256 // 定义一个阈值,用于区分小切片和大切片 + + // 如果旧容量小于阈值,则直接将新容量设置为旧容量的两倍 + // 这种策略适用于小切片,可以快速扩容,减少扩容次数 + if oldCap < threshold { + return doublecap + } + + // 对于大切片,使用更平滑的扩容策略,避免过度分配内存 + // 从 2 倍增长过渡到 1.25 倍增长。 此公式给出了两者之间的平滑过渡。 + for { + // 每次循环,将新容量增加 (newcap + 3*threshold) / 4 + // 相当于 newcap 增加 1/4 的比例,再加上 3/4 的 threshold(256),即 192 + // 这样可以在一定程度上减少内存浪费,并保证切片的增长 + newcap += (newcap + 3*threshold) >> 2 + + // Check for overflow and determine if the new calculated capacity + // is greater or equal to the required new length. + // newLen is guaranteed to be larger than zero, hence + // when newcap overflows then `uint(newcap) > uint(newLen)`. + // This allows to check for both with the same comparison. + + // 我们需要检查`newcap >= newLen`以及`newcap`是否溢出。 + // 保证 newLen 大于零,因此当 newcap 溢出时,'uint(newcap) > uint(newLen)'。 + // 这允许使用相同的比较来检查两者。 + + // 检查新容量是否大于等于所需的新长度,并且检查是否发生了溢出 + if uint(newcap) >= uint(newLen) { + break // 如果新容量足够大,或者发生了溢出,则退出循环 + } + } + + // 当新容量计算溢出时,将新容量设置为请求的容量。 + // 如果计算过程中发生了溢出,则直接将新容量设置为所需的新长度,以确保切片能够容纳所有元素 + if newcap <= 0 { + return newLen + } + + return newcap // 返回计算得到的新容量 +} +``` + +# Golang 切片原理 + +![](https://blog.meowrain.cn/api/i/2025/01/27/STHBnZ1737969258402080877.avif) + +![](https://blog.meowrain.cn/api/i/2025/01/27/L5OPBU1737969429035465587.avif) + +## 扩容规律 + +![](https://blog.meowrain.cn/api/i/2025/01/27/my5VWv1737969803395420365.avif) + +## 切片作为参数 + +Go 语言的函数参数传递,只有值传递,没有引用传递,切片作为参数也是如此 + +我们来验证这一点 + +![](https://blog.meowrain.cn/api/i/2025/01/27/34ZRq21737970293711745015.avif) + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(sl) + fmt.Println(sl) +} + +func f(sl []int) { + for i := 0; i < 3; i++ { + sl = append(sl, i) + } + fmt.Println(sl) +} + +``` + +可以看到,输出的 sl 的值是不一样的,也就是说,f 函数没能修改主函数中的 sl 变量,而只是修改了形参 sl 变量的内容 + +当我们传递一个切片给函数的时候,函数接收到的其实是这个切片的一个副本,但是他们的 array 字段指向的是同一个底层数组。 + +这意味着,如果我们修改底层数组,是会影响到实参和形参的。 + +我们看下面的例子:形参通过改变底层数组影响实参 + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(sl) + fmt.Println(sl) +} + +func f(sl []int) { + sl[1] = 1 + sl[2] = 2 +} + +``` + +![](https://blog.meowrain.cn/api/i/2025/01/27/f395pe1737970003488259606.avif) + +### 通过指针传递影响实参 + +```go +package main + +import "fmt" + +func main() { + sl := []int{6, 6, 6} + f(&sl) + fmt.Println(sl) +} + +func f(sl *[]int) { + *sl = append(*sl, 200) +} + + +``` + +![](https://blog.meowrain.cn/api/i/2025/01/27/igiBeJ1737970227764617103.avif) diff --git a/src/content/posts/Golang/Golang垃圾回收机制.md b/src/content/posts/Golang/Golang垃圾回收机制.md new file mode 100644 index 0000000..3038ad9 --- /dev/null +++ b/src/content/posts/Golang/Golang垃圾回收机制.md @@ -0,0 +1,244 @@ +--- +title: Golang垃圾回收机制 +published: 2025-07-19 +description: '' +image: 'https://blog.meowrain.cn/api/i/2025/07/19/uje4vo-1.webp' +tags: [垃圾回收, Golang, GC] +category: 'Go' +draft: false +lang: '' +--- + +# Go GC机制 + +> [5、Golang三色标记混合写屏障GC模式全分析 (yuque.com)](https://www.yuque.com/aceld/golang/zhzanb#77fdf35b) + +> 垃圾回收(Garbage Collection,简称GC)是编程语言中提供的自动的内存管理机制,自动释放不需要的内存对象,让出存储器资源。GC过程中无需程序员手动执行。GC机制在现代很多编程语言都支持,GC能力的性能与优劣也是不同语言之间对比度指标之一。 + +## 发展过程 + +Go V1.3之前的标记-清除(mark and sweep)算法,Go V1.3之前的标记-清扫(mark and sweep)的缺点 + +## Go V1.3之前的标记-清除(mark and sweep)算法 + +![image-20240709121221919](https://blog.meowrain.cn/api/i/2024/07/09/C6W4Y71720498342584015950.webp) + +接下来我们来看一下在Golang1.3之前的时候主要用的普通的标记-清除算法,此算法主要有两个主要的步骤: + +- 标记(Mark phase) +- 清除(Sweep phase) + +![image-20240709120731505](https://blog.meowrain.cn/api/i/2024/07/09/ANh9c11720498052447247658.webp) + +![image-20240709120757145](https://blog.meowrain.cn/api/i/2024/07/09/yWrUwk1720498077557020958.webp) + +> STW会对可达对象做上标记,然后对不可达对象进行GC回收 + +![image-20240709120900088](https://blog.meowrain.cn/api/i/2024/07/09/29Wcxv1720498140387778591.webp) + +> 操作非常简单,但是有一点需要额外注意:mark and sweep算法在执行的时候,需要程序暂停!即 `STW(stop the world)`,STW的过程中,CPU不执行用户代码,全部用于垃圾回收,这个过程的影响很大,所以STW也是一些回收机制最大的难题和希望优化的点。所以在执行第三步的这段时间,程序会暂定停止任何工作,卡在那等待回收执行完毕。 + +### mark and sweep 算法 缺点 + +1. STW会让程序暂停,使程序出现卡顿(重要问题) +2. 标记需要扫描整个heap +3. 清除数据会产生heap碎片 + +stw暂停范围 + +![image-20240709121953696](https://blog.meowrain.cn/api/i/2024/07/09/kMFipT1720498794174933847.webp) + +从上图来看,全部的GC时间都是包裹在STW范围之内的,这样貌似程序暂停的时间过长,影响程序的运行性能。所以Go V1.3 做了简单的优化,将STW的步骤提前, 减少STW暂停的时间范围.如下所示 + +![54-STW2.png](https://blog.meowrain.cn/api/i/2024/07/09/rI4lNh1720498833454407229.webp) + +上图主要是将STW的步骤提前了一步,因为在Sweep清除的时候,可以不需要STW停止,因为这些对象已经是不可达对象了,不会出现回收写冲突等问题。 + +但是无论怎么优化,Go V1.3都面临这个一个重要问题,就是**mark-and-sweep 算法会暂停整个程序** 。 + +Go是如何面对并这个问题的呢?接下来G V1.5版本 就用**三色并发标记法**来优化这个问题. + +## GoV1.5三色标记法 + +![image-20240709122423404](https://blog.meowrain.cn/api/i/2024/07/09/u0ZJ951720499063811507708.webp) + +![image-20240709122647686](https://blog.meowrain.cn/api/i/2024/07/09/MRhIFy1720499208514108528.webp) + +![image-20240709122753872](https://blog.meowrain.cn/api/i/2024/07/09/Z6DyjS1720499274479089970.webp) + +![image-20240709122920596](https://blog.meowrain.cn/api/i/2024/07/09/OPgFix1720499361118341644.webp) + +![image-20240709123017964](https://blog.meowrain.cn/api/i/2024/07/09/ZkEIjD1720499418393168076.webp) + +![image-20240709123108479](https://blog.meowrain.cn/api/i/2024/07/09/wULnvE1720499469045471792.webp) + +![image-20240709123127729](https://blog.meowrain.cn/api/i/2024/07/09/VpPh5n1720499488250837040.webp) + +![image-20240709123144258](https://blog.meowrain.cn/api/i/2024/07/09/lGPm8C1720499504716064921.webp) + +![image-20240709123228889](https://blog.meowrain.cn/api/i/2024/07/09/qkpbys1720499549310981229.webp) + +## 三色标记法无STW的问题 + +我们加入如果没有STW,那么也就不会再存在性能上的问题,那么接下来我们假设如果三色标记法不加入STW会发生什么事情? +我们还是基于上述的三色并发标记法来说, 他是一定要依赖STW的. 因为如果不暂停程序, 程序的逻辑改变对象引用关系, 这种动作如果在标记阶段做了修改,会影响标记结果的正确性,我们来看看一个场景,如果三色标记法, 标记过程不使用STW将会发生什么事情? + +我们把初始状态设置为已经经历了第一轮扫描,目前黑色的有对象1和对象4, 灰色的有对象2和对象7,其他的为白色对象,且对象2是通过指针p指向对象3的,如图所示。 + +![55-三色标记问题1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/V3y0mh1720502068945434434.webp) + +现在如何三色标记过程不启动STW,那么在GC扫描过程中,任意的对象均可能发生读写操作,如图所示,在还没有扫描到对象2的时候,已经标记为黑色的对象4,此时创建指针q,并且指向白色的对象3。 + +![56-三色标记问题2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/FKfHeh1720502103967556957.webp) + +与此同时灰色的对象2将指针p移除,那么白色的对象3实则就是被挂在了已经扫描完成的黑色的对象4下,如图所示。 + +![57-三色标记问题3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/vja2PL1720502115722049746.webp) + +然后我们正常指向三色标记的算法逻辑,将所有灰色的对象标记为黑色,那么对象2和对象7就被标记成了黑色,如图所示。 + +![58-三色标记问题4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/w2ane51720502140258068700.webp) + +那么就执行了三色标记的最后一步,将所有白色对象当做垃圾进行回收,如图所示。 + +![59-三色标记问题5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bzlj9c1720502156829093691.webp) + +但是最后我们才发现,本来是对象4合法引用的对象3,却被GC给“误杀”回收掉了。 + +### GC误杀条件 + +可以看出,有两种情况,在三色标记法中,是不希望被发生的。 + +- 条件1: 一个白色对象被黑色对象引用**(白色被挂在黑色下)** +- 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏**(灰色同时丢了该白色)** + 如果当以上两个条件同时满足时,就会出现对象丢失现象! + +## 屏障机制 + +> 为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是**STW的过程有明显的资源浪费,对所有的用户程序都有很大影响**。那么是否可以在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?答案是可以的,我们只要使用一种机制,尝试去破坏上面的两个必要条件就可以了。 + +![image-20240709132144714](https://blog.meowrain.cn/api/i/2024/07/09/2cO2yL1720502505096278545.webp) + +### 强三色不变式 + +强制性的不允许黑色对象引用白色对象 + +> 破坏条件1 + +![image-20240709131813359](https://blog.meowrain.cn/api/i/2024/07/09/iZbHhI1720502294165051623.webp) + +### 弱三色不变式 + +黑色对象可以引用白色对象,但是要保证白色独享存在其它灰色对象对它的引用,或者可达它的链路上游存在灰色对象 + +> 破坏条件2 + +![image-20240709132012351](https://blog.meowrain.cn/api/i/2024/07/09/SwzQBu1720502412929353413.webp) + +为了遵循上述的两个方式,GC算法演进到两种屏障方式,他们“插入屏障”, “删除屏障”。 + +![image-20240709133322663](https://blog.meowrain.cn/api/i/2024/07/09/JjkcAo1720503203424780995.webp) + +### 插入屏蔽 + +> 不在栈上使用 + +`具体操作`: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色) + +`满足`: **强三色不变式**. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色) + +```go +添加下游对象(当前下游对象slot, 新下游对象ptr) { + //1 + 标记灰色(新下游对象ptr) + + //2 + 当前下游对象slot = 新下游对象ptr +} +``` + +这里说一下这个过程,首先因为插入屏障不在栈上使用 + +下面的图里面,已经进行了一次三色标记,外界向对象4添加对象8,对象1添加对象9,但是我们知道,对象1在栈上,所以它不会应用插入屏障,也就是说,这个时候对象 9不会按照插入屏障的规则设置为灰色,而对象4在堆上,因此它会应用插入屏障,所以会把对象8设置为灰色,然后我们进行第二次三色标记,从灰色对象出发(对象2,对象7,对象8) ,找可达对象(对象3),因此将对象3设置为灰色,然后对象2,7,8设置为黑色,接着进行第三次三色标记,从灰色对象出发(对象3),发现没有可达对象,因此设置对象3为黑色,这个时候我们有黑色对象: 对象1,对象2,对象3,对象4,对象7,对象8. + +按照常理我们这个时候应该进行垃圾回收了对吧,其实不然,我们这个时候要把栈空间的对象全部设置为白色,然后使用STW暂停栈空间(对象1,对象2,对象3,对象9,对象5),防止外界干扰(再有对象被添加到黑色对象下) + +然后我们对栈空间重新进行一次三色标记,直到没有灰色对象 + +过程如下: + +从对象1出发,设置对象1为灰色,接下来看从对象1走的可达对象,发现可达对象有对象2和对象9,因此我们把对象2和对象9设置为灰色对象,把对象1设置为黑色对象,然后我们再从灰色对象出发(对象2和对象9),发现对象2可达对象3,对象9没有可达对象,因此把对象3设置为灰色对象,对象2,9设置为黑色对象,接下来从灰色对象(此时只有对象3)出发,发现对象3没有可达对象,设置对象3为黑色对象。至此栈里面已经没有灰色对象,我们先暂停STW,然后进行最后的GC回收,可以发现白色对象只有 对象5,对象6,因此对白色对象进行清除。 + +至此,GC三色标记并发情况下的插入屏障流程完毕 + +![image-20240709135123289](https://blog.meowrain.cn/api/i/2024/07/09/LKKoCr1720504284631136649.webp) + +![image-20240709135153851](https://blog.meowrain.cn/api/i/2024/07/09/c9akf61720504314509112134.webp) + +![image-20240709135240616](https://blog.meowrain.cn/api/i/2024/07/09/9ggDq01720504361239129518.webp) + +![image-20240709135330243](https://blog.meowrain.cn/api/i/2024/07/09/brrrcs1720504410886565715.webp) + +![image-20240709135410526](https://blog.meowrain.cn/api/i/2024/07/09/huazYX1720504451233838741.webp) + +![image-20240709135448742](https://blog.meowrain.cn/api/i/2024/07/09/WENeFq1720504489239707269.webp) + +![image-20240709135535312](https://blog.meowrain.cn/api/i/2024/07/09/AYQ3tv1720504535821911058.webp) + +### 删除屏蔽 + +`具体操作`: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。 + +`满足`: **弱三色不变式**. (保护灰色对象到白色对象的路径不会断) + +``` +添加下游对象(当前下游对象slot, 新下游对象ptr) { + //1 + if (当前下游对象slot是灰色 || 当前下游对象slot是白色) { + 标记灰色(当前下游对象slot) //slot为被删除对象, 标记为灰色 + } + + //2 + 当前下游对象slot = 新下游对象ptr +} +``` + +![72-三色标记删除写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/YIsQlm1720506425416637589.webp) + +![73-三色标记删除写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/l7zqib1720506436481765589.webp) +![74-三色标记删除写屏障3.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/9CZbQB1720506459636243158.webp) + +![75-三色标记删除写屏障4.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/jsDCqs1720506469140624748.webp) + +![76-三色标记删除写屏障5.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/ccDlph1720506476790209274.webp) + +![77-三色标记删除写屏障6.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/zWf7Gz1720506482597765808.webp) + +![78-三色标记删除写屏障7.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/bLHxhy1720506492796935675.webp) + +这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。 + +### 混合屏障Go V1.8 + +插入写屏障和删除写屏障的短板: + +● 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活; +● 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。 + +Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。 + +![image-20240709142925523](https://blog.meowrain.cn/api/i/2024/07/09/mIzEEG1720506565775368039.webp) + +![79-三色标记混合写屏障1.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/WfkvFx1720506886093721996.webp) + +![80-三色标记混合写屏障2.jpeg](https://blog.meowrain.cn/api/i/2024/07/09/mhPr4L1720506893765506689.webp) + +`具体操作`: + +1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW), + +2、GC期间,任何在栈上创建的新对象,均为黑色。 + +3、被删除的对象标记为灰色。 + +4、被添加的对象标记为灰色。 diff --git a/src/content/posts/操作系统/IO多路复用技术.md b/src/content/posts/操作系统/IO多路复用技术.md index bd0014c..5f9d38e 100644 --- a/src/content/posts/操作系统/IO多路复用技术.md +++ b/src/content/posts/操作系统/IO多路复用技术.md @@ -11,7 +11,7 @@ lang: '' # 参考资料 -![万字图解| 深入揭秘IO多路复用](https://cloud.tencent.com/developer/article/2383534) +[万字图解| 深入揭秘IO多路复用](https://cloud.tencent.com/developer/article/2383534) # 为什么要有IO多路复用技术? diff --git a/src/content/posts/设计模式/golang设计模式.md b/src/content/posts/设计模式/golang设计模式.md new file mode 100644 index 0000000..406c873 --- /dev/null +++ b/src/content/posts/设计模式/golang设计模式.md @@ -0,0 +1,2360 @@ +--- +title: golang设计模式 +published: 2024-08-07 +description: '' +image: '' +tags: [设计模式, golang] +category: '设计模式' +draft: false +lang: '' +--- +# Golang设计模式 + +# Golang设计模式 + +# 一.面向对象设计原则 + +![image-20240801214903765](https://blog.meowrain.cn/api/i/2024/08/01/Xag4Ld1722520145404590356.webp)​ + +## 1.1 单一职责原则 + +> 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个。 + +```go +//不遵守单一职责原则 +package main + +import "fmt" + +/* +在这个例子中,Clothes 类包含了两个方法 onWork() 和 onShop(),这两个方法描述了在不同场景下(工作和购物)穿着相同的装扮。 +问题在于这两种场景虽然使用了相同的装扮, +但它们实际上是两种不同的行为或上下文。 +*/ +type Clothes struct{} + +func (c *Clothes) onWork() { + fmt.Println("工作的装扮") +} +func (c *Clothes) onShop() { + fmt.Println("购物的装扮") +} +func main() { + c := Clothes{} + //逛街的业务 + c.onShop() + //工作的业务 + c.onWork() +} + +``` + +```go +//这个修改后的代码确实更好地遵守了单一职责原则。现在有两个不同的类,WorkClothes 和 ShopClothes,它们分别负责工作和购物时的装扮。每个类都有一个明确的职责,即定义了在某个特定场合下的装扮。 + +package main + +import "fmt" + +type ShopClothes struct { +} +type WorkClothes struct{} + +func (c *WorkClothes) onWork() { + fmt.Println("工作的装扮") +} +func (c *ShopClothes) onShop() { + fmt.Println("购物的装扮") +} +func main() { + c := &WorkClothes{} + c.onWork() + c1 := &ShopClothes{} + c1.onShop() +} + +``` + +在面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就只限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂。 + +## 1.2 开闭原则 + +开闭原则(Open-Closed Principle, OCP)是面向对象设计中的一个重要原则,它指出软件实体(如类、模块、函数等)应该是对扩展开放的,对修改关闭的。这意味着我们可以扩展一个类的行为而不修改它的源代码。 + +```go +//不满足开闭原则 +package main + +import "fmt" + +// 假设我们有一个系统用于处理不同类型的订单,例如普通订单和紧急订单。最初系统只支持普通订单,后来需要增加对紧急订单的支持。 + +type Order struct { +} + +func (o *Order) Process() { + fmt.Println("处理普通订单") +} +func main() { + o := &Order{} + o.Process() +} + +--- + +package main +import "fmt" + +type Order struct{} + +func (o *Order) Process(){ + fmt.Println("处理普通订单") +} +func (o *Order) ProcessUrgently() { + fmt.Println("处理紧急订单") +} + +func main(){ + o := &Order{} + o.Process() + o.ProcessUrgently() +} +``` + +这个修改违反了开闭原则,因为我们直接修改了原有的 `Order`​ 类来添加新的功能。如果以后需要添加更多类型的订单处理逻辑(如VIP订单处理),我们可能还需要继续修改 `Order`​ 类,这会导致代码难以维护。 + +```go +package main + +import "fmt" + +type OrderHandler interface { + Handle() +} + +type NormalOrder struct{} + +func (n *NormalOrder) Handle() { + fmt.Println("处理普通订单") +} + +type UrgentOrder struct{} + +func (u *UrgentOrder) Handle() { + fmt.Println("紧急处理订单") +} + +func main() { + normalOrder := &NormalOrder{} + normalOrder.Handle() + + urgentOrder := &UrgentOrder{} + urgentOrder.Handle() +} +``` + +1. **定义接口**: + + * 我们定义了一个 `OrderHandler`​ 接口,其中包含一个 `Handle`​ 方法。 +2. **实现接口**: + + * `NormalOrder`​ 结构体实现了 `OrderHandler`​ 接口,并定义了 `Handle`​ 方法来处理普通订单。 + * `UrgentOrder`​ 结构体同样实现了 `OrderHandler`​ 接口,并定义了 `Handle`​ 方法来处理紧急订单。 +3. **扩展性**: + + * 如果需要添加新的订单类型(如 VIP 订单),我们只需创建一个新的结构体来实现 `OrderHandler`​ 接口,并提供相应的 `Handle`​ 方法实现即可。 + * 这意味着我们可以扩展系统,而不需要修改现有代码。 + +## 1.3 依赖倒置原则 + +依赖倒置原则(Dependency Inversion Principle, DIP)是一种面向对象设计的原则,它提倡高层模块不应该依赖于低层模块,二者都应该依赖于抽象;抽象不应该依赖于细节,细节应该依赖于抽象。 + +依赖倒置原则鼓励我们使用接口或抽象基类来定义模块之间的交互,而不是直接依赖于具体的实现。这样可以降低系统的耦合度,提高模块的复用性和灵活性。 + +### 示例:发送通知系统 + +假设我们有一个应用需要发送通知给用户,这些通知可以是电子邮件、短信或推送通知。我们将使用依赖倒置原则来设计这个系统。 + +1. **定义接口**: + + * 我们定义了一个 `Notifier`​ 接口,它包含一个 `SendNotification`​ 方法,用于发送通知。 +2. **实现接口**: + + * `EmailNotifier`​ 结构体实现了 `Notifier`​ 接口,并定义了 `SendNotification`​ 方法来发送电子邮件通知。 + * `SmsNotifier`​ 结构体实现了 `Notifier`​ 接口,并定义了 `SendNotification`​ 方法来发送短信通知。 + * `PushNotifier`​ 结构体实现了 `Notifier`​ 接口,并定义了 `SendNotification`​ 方法来发送推送通知。 +3. **使用依赖注入**: + + * `NotificationService`​ 结构体通过构造函数接收一个 `Notifier`​ 接口类型的参数,这样可以根据需要传入不同的通知实现。 +4. **扩展性**: + + * 如果需要添加新的通知方式(如电话通知),我们只需创建一个新的结构体来实现 `Notifier`​ 接口,并提供相应的 `SendNotification`​ 方法实现即可。 + * 这意味着我们可以扩展系统,而不需要修改现有代码。 + +通过这种方式,我们遵循了依赖倒置原则: + +* **高层模块依赖于抽象**:`NotificationService`​ 依赖于 `Notifier`​ 接口,而不是具体的实现。 +* **抽象不依赖于细节**:`Notifier`​ 接口定义了通知的基本行为,而具体的实现(如 `EmailNotifier`​, `SmsNotifier`​, `PushNotifier`​)依赖于这个接口。 +* **低层模块依赖于抽象**:每个具体的实现都实现了 `Notifier`​ 接口,从而依赖于抽象。 + +```go +package main + +import "fmt" + +type Notifier interface { + SendNotification(string) +} + +type SmsNotifier struct { +} +type EmailNotifier struct { +} +type WechatNotifier struct{} + +// 定义一个通知服务,使用依赖注入的方式接收通知器 +type NotificationService struct { + notifier Notifier +} + +func (sms *SmsNotifier) SendNotification(msg string) { + fmt.Println("send sms", msg) +} +func (email *EmailNotifier) SendNotification(msg string) { + fmt.Println("send email", msg) +} +func (wechat *WechatNotifier) SendNotification(msg string) { + fmt.Println("send wechat", msg) +} + +func NewNotificationService(notifier *Notifier) *NotificationService { + return &NotificationService{ + notifier: *notifier, + } +} +func (ns *NotificationService) Notify() { + ns.notifier.SendNotification("meowrain") +} + +func main() { + sms := &SmsNotifier{} + email := &EmailNotifier{} + wechat := &WechatNotifier{} + + ns1 := NewNotificationService(&sms) + ns2 := NewNotificationService(&email) + ns3 := NewNotificationService(&wechat) + + ns1.Notify() + ns2.Notify() + ns3.Notify() +} + +``` + +## 1.4 合成复用原则 + +合成复用原则(Composite Reuse Principle, CRP)提倡使用对象组合而非继承来实现复用。也就是说,我们应该优先考虑通过对象的组合来重用现有代码,而不是通过继承来重用代码。这样可以减少继承体系的复杂性,并提高系统的灵活性。 + +假设我们有一个系统需要处理各种类型的文档,例如 PDF 文档和 Word 文档。我们可以使用组合的方式来实现这些文档的处理。 + +```go +package main + +import "fmt" + +// 定义一个通用的文档处理器接口 +type DocumentProcessor interface { + Process() +} + +// 实现 PDF 文档处理器 +type PdfDocumentProcessor struct{} + +func (p *PdfDocumentProcessor) Process() { + fmt.Println("处理 PDF 文档") +} + +// 实现 Word 文档处理器 +type WordDocumentProcessor struct{} + +func (w *WordDocumentProcessor) Process() { + fmt.Println("处理 Word 文档") +} + +// 定义一个文档处理器服务,使用组合的方式包含具体的处理器 +type DocumentService struct { + processor DocumentProcessor +} + +func NewDocumentService(processor DocumentProcessor) *DocumentService { + return &DocumentService{processor: processor} +} + +func (ds *DocumentService) ProcessDocument() { + ds.processor.Process() +} + +func main() { + pdfProcessor := &PdfDocumentProcessor{} + wordProcessor := &WordDocumentProcessor{} + + pdfService := NewDocumentService(pdfProcessor) + wordService := NewDocumentService(wordProcessor) + + pdfService.ProcessDocument() + wordService.ProcessDocument() +} +``` + +### 解释 + +1. **定义接口**: + + * 我们定义了一个 `DocumentProcessor`​ 接口,它包含一个 `Process`​ 方法,用于处理文档。 +2. **实现接口**: + + * `PdfDocumentProcessor`​ 结构体实现了 `DocumentProcessor`​ 接口,并定义了 `Process`​ 方法来处理 PDF 文档。 + * `WordDocumentProcessor`​ 结构体实现了 `DocumentProcessor`​ 接口,并定义了 `Process`​ 方法来处理 Word 文档。 +3. **使用组合**: + + * `DocumentService`​ 结构体通过构造函数接收一个 `DocumentProcessor`​ 接口类型的参数,这样可以根据需要传入不同的文档处理器实现。 +4. **扩展性**: + + * 如果需要添加新的文档类型(如 TXT 文档),我们只需创建一个新的结构体来实现 `DocumentProcessor`​ 接口,并提供相应的 `Process`​ 方法实现即可。 + * 这意味着我们可以扩展系统,而不需要修改现有代码。 + +通过这种方式,我们遵循了合成复用原则: + +* **使用组合而非继承**:`DocumentService`​ 通过组合 `DocumentProcessor`​ 实现来处理不同的文档类型,而不是通过继承来复用代码。 +* **提高了灵活性**:如果需要添加新的文档类型,只需添加新的处理器实现即可,而无需修改现有代码 + +## 1.5 迪米特法则 + +一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性。例如在一个程序中,各个模块之间相互调用时,通常会提供一个统一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变时,不会影响其他模块的使用。(黑盒原理) + +### 示例:聊天室系统 + +假设我们有一个简单的聊天室系统,用户可以发送消息给其他人。我们可以使用中介者模式来避免用户对象直接相互引用,从而降低耦合度。 + +如果这个聊天室系统的代码没有遵循迪米特法则,那么用户对象可能会直接相互引用,导致对象之间的耦合度增加。下面是一个未遵循迪米特法则的版本,用户对象直接向其他用户发送消息: + +```go +package main + +import "fmt" + +// 定义用户 +type User struct { + name string + friends []User +} + +func (u *User) AddFriend(friend User) { + u.friends = append(u.friends, friend) +} + +func (u *User) Send(message string) { + fmt.Printf("%s: 发送消息 '%s'\n", u.name, message) + for _, friend := range u.friends { + friend.Receive(message) + } +} + +func (u *User) Receive(message string) { + fmt.Printf("%s: 收到消息 '%s'\n", u.name, message) +} + +func NewUser(name string) *User { + return &User{name: name, friends: make([]User, 0)} +} + +func main() { + alice := NewUser("Alice") + bob := NewUser("Bob") + charlie := NewUser("Charlie") + + alice.AddFriend(*bob) + alice.AddFriend(*charlie) + bob.AddFriend(*alice) + bob.AddFriend(*charlie) + charlie.AddFriend(*alice) + charlie.AddFriend(*bob) + + alice.Send("你好,大家!") + bob.Send("嗨,Alice!") + charlie.Send("很高兴见到你们!") +} +``` + +使用中介者模式可以解决这个问题 + +```go +package main + +import "fmt" + +// 定义中介者接口 +type Mediator interface { + SendMessage(message string, user User) +} + +// 定义用户接口 +type User interface { + Send(message string) + Receive(message string) +} + +// 实现中介者 +type ChatRoom struct{} + +func (cr *ChatRoom) SendMessage(message string, user User) { + fmt.Printf("消息 '%s' 发送给所有用户\n", message) + for _, u := range users { + if u != user { + u.Receive(message) + } + } +} + +var users = make([]User, 0) + +func (cr *ChatRoom) AddUser(user User) { + users = append(users, user) +} + +// 实现用户 +type UserImpl struct { + name string +} + +func (u *UserImpl) Send(message string) { + fmt.Printf("%s: 发送消息 '%s'\n", u.name, message) + chatRoom.SendMessage(message, u) +} + +func (u *UserImpl) Receive(message string) { + fmt.Printf("%s: 收到消息 '%s'\n", u.name, message) +} + +func NewUser(name string) *UserImpl { + return &UserImpl{name: name} +} + +func main() { + chatRoom := &ChatRoom{} + + alice := NewUser("Alice") + bob := NewUser("Bob") + charlie := NewUser("Charlie") + + chatRoom.AddUser(alice) + chatRoom.AddUser(bob) + chatRoom.AddUser(charlie) + + alice.Send("你好,大家!") + bob.Send("嗨,Alice!") + charlie.Send("很高兴见到你们!") +} +``` + +### 解释 + +1. **定义接口**: + + * 我们定义了一个 `Mediator`​ 接口,它包含一个 `SendMessage`​ 方法,用于转发消息。 + * 我们还定义了一个 `User`​ 接口,它包含一个 `Send`​ 方法用于发送消息和一个 `Receive`​ 方法用于接收消息。 +2. **实现接口**: + + * `ChatRoom`​ 结构体实现了 `Mediator`​ 接口,并定义了 `SendMessage`​ 方法来转发消息。 + * `UserImpl`​ 结构体实现了 `User`​ 接口,并定义了 `Send`​ 和 `Receive`​ 方法来发送和接收消息。 +3. **使用中介者模式**: + + * 用户通过 `ChatRoom`​ 对象发送消息,而不是直接相互引用。 + * `ChatRoom`​ 负责管理用户列表并将消息转发给其他用户。 +4. **扩展性**: + + * 如果需要添加新的用户,只需创建新的 `UserImpl`​ 实例并通过 `ChatRoom`​ 对象进行注册即可。 + +通过这种方式,我们遵循了迪米特法则: + +* **对象之间的交互保持在最小范围内**:用户对象通过 `ChatRoom`​ 中介者进行通信,而不是直接相互引用。 +* **降低了系统的耦合度**:如果需要添加新的用户或改变消息传递的逻辑,只需修改 `ChatRoom`​ 的实现,而无需修改用户对象的代码。 + +# 二.设计模式 + +## 2.1 创建型模式 + +![image-20240801222410868](https://blog.meowrain.cn/api/i/2024/08/01/dRkKgq1722522252544668354.webp)​ + +### 2.1.1 单例模式 + +是保证一个类仅有一个实例,并提供一个访问它的全局访问点。 + +单例模式要解决的问题是: + +保证一个类永远只能有一个对象,且该对象的功能依然能被其他模块使用。 + +![image-20240801222447125](https://blog.meowrain.cn/api/i/2024/08/01/6ooTda1722522288250437417.webp)​ + +![image-20240801223112728](https://blog.meowrain.cn/api/i/2024/08/01/nHoclK1722522673887394449.webp)​ + +```go +package singleton + +import "fmt" + +type instance struct { + name string +} + +var ins *instance = new(instance) + +func (ins *instance) Work() { + fmt.Println("work") +} + +func GetInstance(name string) *instance { + ins.name = name + return ins +} + +``` + +```go +package singletontest + +import "singleton" + +func hello() { + s := singleton.GetInstance() + s.Work() + +} + +``` + +上面代码中,我们提前实例化了instance,然后创建了一个GetInstance方法来获取这个对象 + +在另外一个包里,我们只能通过GetInstance获取这个示例对象并调用它的函数 + +上面代码推演了一个单例的创建和逻辑过程,上述是单例模式中的一种,属于“饿汉式”。含义是,在初始化单例唯一指针的时候,就已经提前开辟好了一个对象,申请了内存。饿汉式的好处是,不会出现线程并发创建,导致多个单例的出现,但是缺点是如果这个单例对象在业务逻辑没有被使用,也会客观的创建一块内存对象。那么与之对应的模式叫“懒汉式”,代码如下: + +```go +package singleton + +import "fmt" + +type instance struct { + name string +} + +var ins *instance + +func (ins *instance) Work() { + fmt.Println("work") +} + +func GetInstance(name string) *instance { + if ins == nil { + ins = new(instance) + ins.name = name + return ins + } + ins.name = name + return ins +} + +``` + +线程安全的单例模式实现 + +上面的“懒汉式”实现是非线程安全的设计方式,也就是如果多个线程或者协程同时首次调用GetInstance()方法有概率导致多个实例被创建,则违背了单例的设计初衷。那么在上面的基础上进行修改,可以利用Sync.Mutex进行加锁,保证线程安全。这种线程安全的写法,有个最大的缺点就是每次调用该方法时都需要进行锁操作,在性能上相对不高效,具体的实现改进如下: + +```go +package singleton + +import ( + "fmt" + "sync" +) + +type instance struct { + name string +} +var lock sync.Mutex +var ins *instance + +func (ins *instance) Work() { + fmt.Println("work") +} + +func GetInstance(name string) *instance { + lock.Lock() + defer lock.Unlock() + if ins == nil { + ins = new(instance) + ins.name = name + return ins + } + ins.name = name + return ins +} + +``` + +上面代码虽然解决了线程安全,但是每次调用GetInstance()都要加锁会极大影响性能。所以接下来可以借助"sync/atomic"来进行内存的状态存留来做互斥。atomic就可以自动加载和设置标记,代码如下: + +```go +package singleton + +import ( + "fmt" + "sync" + "sync/atomic" +) + +type instance struct { + name string +} + +var initialized uint32 +var lock sync.Mutex +var ins *instance + +func (ins *instance) Work() { + fmt.Println("work") +} + +func GetInstance(name string) *instance { + //如果标记为被设置,直接返回,不加锁 + if atomic.LoadUint32(&initialized) == 1 { + ins.name = name + return ins + } + //如果没有,则加锁申请 + lock.Lock() + defer lock.Unlock() + + if initialized == 0 { + ins = new(instance) + ins.name = name + //设置标记位 + atomic.StoreUint32(&initialized, 1) + } + return ins +} + +``` + +述的实现其实Golang有个方法已经帮助开发者实现完成,就是Once模块,来看下Once.Do()方法的源代码 + +```go +func (o *Once) Do(f func()) {   //判断是否执行过该方法,如果执行过则不执行 + if atomic.LoadUint32(&o.done) == 1 { + return + } + // Slow-path. + o.m.Lock() + defer o.m.Unlock()   + if o.done == 0 { + defer atomic.StoreUint32(&o.done, 1) + f() + } +} +``` + +```go +package singleton + +import ( + "fmt" + "sync" +) + +type instance struct { + name string +} + +var once sync.Once +var ins *instance + +func (ins *instance) Work() { + fmt.Println("work") +} + +func GetInstance(name string) *instance { + once.Do(func() { + ins = new(instance) + ins.name = name + }) + return ins +} + +``` + +#### 优缺点 + +优点: + +(1) 单例模式提供了对唯一实例的受控访问。 + +(2) 节约系统资源。由于在系统内存中只存在一个对象。 + +缺点: + +(1) 扩展略难。单例模式中没有抽象层。 + +(2) 单例类的职责过重。 + +#### 适用场景 + +(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。 + +(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。 + +### 2.1.2 简单工厂模式 + +```go +package simplefactory + +import "fmt" + +type Fruit struct { +} + +func (f *Fruit) Show(name string) { + if name == "apple" { + fmt.Println("我是苹果") + } else if name == "banana" { + fmt.Println("我是香蕉") + } else if name == "pear" { + fmt.Println("我是梨") + } +} +func NewFruit(name string) *Fruit { + fruit := new(Fruit) + + if name == "apple" { + //创建apple逻辑 + } else if name == "banana" { + //创建banana逻辑 + } else if name == "pear" { + //创建pear逻辑 + } + + return fruit +} +func main() { + apple := NewFruit("apple") + apple.Show("apple") + + banana := NewFruit("banana") + banana.Show("banana") + + pear := NewFruit("pear") + pear.Show("pear") +} + +``` + +不难看出,Fruit类是一个“巨大的”类,在该类的设计中存在如下几个问题: + + (1) 在Fruit类中包含很多“if…else…”代码块,整个类的代码相当冗长,代码越长,阅读难度、维护难度和测试难度也越大;而且大量条件语句的存在还将影响系统的性能,程序在执行过程中需要做大量的条件判断。 + + (2) Fruit类的职责过重,它负责初始化和显示所有的水果对象,将各种水果对象的初始化代码和显示代码集中在一个类中实现,违反了“单一职责原则”,不利于类的重用和维护; + +(3) 当需要增加新类型的水果时,必须修改Fruit类的构造函数NewFruit()和其他相关方法源代码,违反了“开闭原则”。 + +![image-20240801235215033](https://blog.meowrain.cn/api/i/2024/08/01/V1WfvS1722527536699869600.webp)​ + +简单工厂模式并不属于GoF的23种设计模式。他是开发者自发认为的一种非常简易的设计模式,其角色和职责如下: + + **工厂(Factory)角色**:简单工厂模式的核心,它负责实现创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需的产品对象。 + + **抽象产品(AbstractProduct)角色**:简单工厂模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 + + **具体产品(Concrete Product)角色**:简单工厂模式所创建的具体实例对象。 + +![image-20240801235341994](https://blog.meowrain.cn/api/i/2024/08/01/AXtiza1722527623206165692.webp)​ + +实现: + +![image-20240801235404494](https://blog.meowrain.cn/api/i/2024/08/01/XT15Ff1722527645683519846.webp)​ + +```go +package simplefactory + +type Factory struct{} + +func (f *Factory) CreateFruit(name string) Fruit { + switch name { + case "apple": + return &Apple{} + case "banana": + return &Banana{} + default: + return nil + } +} + +type Fruit interface { + Show() +} +type Apple struct{} +type Banana struct{} + +func (a *Apple) Show() { + println("this is apple") +} +func (b *Banana) Show() { + println("this is banana") +} +func main() { + var fac Factory = Factory{} + apple := fac.CreateFruit("apple") + apple.Show() + banana := fac.CreateFruit("banana") + banana.Show() + +} + +``` + +### 2.1.3 工厂方法模式 + +**抽象工厂(Abstract Factory)角色**:工厂方法模式的核心,任何工厂类都必须实现这个接口。 + +**工厂(Concrete Factory)角色**:具体工厂类是抽象工厂的一个实现,负责实例化产品对象。 + +**抽象产品(Abstract Product)角色**:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口。 + +**具体产品(Concrete Product)角色**:工厂方法模式所创建的具体实例对象。 + +![image-20240802145422702](https://blog.meowrain.cn/api/i/2024/08/02/K8jVPA1722581664230333525.webp)​ + +实现: + +![image-20240802145506969](https://blog.meowrain.cn/api/i/2024/08/02/p9bFfd1722581708073483343.webp)​ + +```go +package factory + +type Fruit interface { + Show() +} +type Apple struct { +} +type Banana struct{} +type Pear struct{} + +func (a *Apple) Show() { + println("我是苹果") +} +func (b *Banana) Show() { + println("我是香蕉") +} +func (p *Pear) Show() { + println("我是梨") +} + +type FruitFactory interface { + CreateFruit() Fruit +} +type AppleFactory struct { +} +type PearFactory struct { +} +type BananaFactory struct { +} + +func (applefac *AppleFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Apple{} + return fruit +} +func (pearfac *PearFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Pear{} + return fruit +} +func (bananafac *BananaFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Banana{} + return fruit +} + +func main() { + var applefactory FruitFactory + applefactory = new(AppleFactory) + apple := applefactory.CreateFruit() + apple.Show() + + var pearfactory FruitFactory + pearfactory = new(PearFactory) + pear := pearfactory.CreateFruit() + pear.Show() + + var bananafactory FruitFactory + bananafactory = new(BananaFactory) + banana := bananafactory.CreateFruit() + banana.Show() +} + +``` + +上述代码是通过面向抽象层开发,业务逻辑层的main()函数逻辑,依然是只与工厂耦合,且只与抽象的工厂和抽象的水果类耦合,这样就遵循了面向抽象层接口编程的原则。 + +那么抽象的工厂方法模式如何体现“开闭原则”的。接下来可以尝试在原有的代码上添加一种新产品的生产,如“日本苹果”,具体的代码如下: + +```go +package factory + +import "fmt" + +type Fruit interface { + Show() +} +type Apple struct { +} +type Banana struct{} +type Pear struct{} + +func (a *Apple) Show() { + println("我是苹果") +} +func (b *Banana) Show() { + println("我是香蕉") +} +func (p *Pear) Show() { + println("我是梨") +} + +type FruitFactory interface { + CreateFruit() Fruit +} +type AppleFactory struct { +} +type PearFactory struct { +} +type BananaFactory struct { +} + +func (applefac *AppleFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Apple{} + return fruit +} +func (pearfac *PearFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Pear{} + return fruit +} +func (bananafac *BananaFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &Banana{} + return fruit +} + +// (+) 新增一个"日本苹果" +type JapanApple struct { +} +type JapanAppleFactory struct{} + +func (jp *JapanApple) Show() { + fmt.Println("我是日本苹果") +} +func (jpapple *JapanAppleFactory) CreateFruit() Fruit { + var fruit Fruit + fruit = &JapanApple{} + return fruit +} +func main() { + var applefactory FruitFactory + applefactory = new(AppleFactory) + apple := applefactory.CreateFruit() + apple.Show() + + var pearfactory FruitFactory + pearfactory = new(PearFactory) + pear := pearfactory.CreateFruit() + pear.Show() + + var bananafactory FruitFactory + bananafactory = new(BananaFactory) + banana := bananafactory.CreateFruit() + banana.Show() + + var jpapplefactory FruitFactory + jpapplefactory = new(JapanAppleFactory) + jpapple := jpapplefactory.CreateFruit() + jpapple.Show() + +} + +``` + +可以看见,新增的基本类“日本苹果”,和“具体的工厂” 均没有改动之前的任何代码。完全符合开闭原则思想。新增的功能不会影响到之前的已有的系统稳定性。 + +工厂方法模式的优缺点优点: + +1. 不需要记住具体类名,甚至连具体参数都不用记忆。 +2. 实现了对象创建和使用的分离。 +3. 系统的可扩展性也就变得非常好,无需修改接口和原类。 + +4.对于新产品的创建,符合开闭原则。 + +缺点: + +1. 增加系统中类的个数,复杂度和理解度增加。 +2. 增加了系统的抽象性和理解难度。 + +适用场景: + +1. 客户端不知道它所需要的对象的类。 +2. 抽象工厂类通过其子类来指定创建哪个对象。 + +### 2.1.4 抽象工厂方法模式 + +工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。因此,可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是本文将要学习的抽象工厂模式的基本思想。 + +![image-20240803195754045](https://blog.meowrain.cn/api/i/2024/08/03/ZoNOYt1722686275770795997.webp)​ + +从工厂方法模式可以看出来: + +(1)当添加一个新产品的时候,比如葡萄,虽然不用修改代码,但是需要添加大量的类,而且还需要添加相对的工厂。(系统开销,维护成本) + +(2)如果使用同一地域的水果(日本苹果,日本香蕉,日本梨),那么需要分别创建具体的工厂,如果选择出现失误,将会造成混乱,虽然可以加一些约束,但是代码实现变得复杂。 + +所以“抽象工厂方法模式”引出了“产品族”和“产品等级结构”概念,其目的是为了更加高效的生产同一个产品组产品。 + +产品族与产品等级结构 + +![image-20240803195849901](https://blog.meowrain.cn/api/i/2024/08/03/0pievr1722686331073243306.webp)​ + +上图表示“产品族”和“产品登记结构”的关系。 + +产品族:具有同一个地区、同一个厂商、同一个开发包、同一个组织模块等,但是具备不同特点或功能的产品集合,称之为是一个产品族。 + +产品等级结构:具有相同特点或功能,但是来自不同的地区、不同的厂商、不同的开发包、不同的组织模块等的产品集合,称之为是一个产品等级结构。 + +当程序中的对象可以被划分为产品族和产品等级结构之后,那么“抽象工厂方法模式”才可以被适用。 + +“抽象工厂方法模式”是针对“产品族”进行生产产品,具体如下图所示。 + +![image-20240803195945189](https://blog.meowrain.cn/api/i/2024/08/03/zYckpX1722686386022548832.webp)​ + +抽象工厂模式的角色和职责 + +抽象工厂(Abstract Factory)角色:它声明了一组用于创建一族产品的方法,每一个方法对应一种产品。 + +具体工厂(Concrete Factory)角色:它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某个产品等级结构中。 + +抽象产品(Abstract Product)角色:它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法。 + +具体产品(Concrete Product)角色:它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法。 + +![image-20240803200223147](https://blog.meowrain.cn/api/i/2024/08/03/cxvnQA1722686544248975631.webp)​ + +可以看出来具体的工厂1,只负责生成具体的产品A1和B1,具体的工厂2,只负责生成具体的产品A2和B2。 + +“工厂1、A1、B1”为一组,是一个产品族, “工厂2、A2、B2”为一组,也是一个产品族。 + +抽象工厂方法模式的实现 + +```go +package abstractfactory + +import "fmt" + +type FruitFactory interface { + CreateApple() Apple + CreateBanana() Banana + CreatePear() Pear +} +type ChinaFruitFactory struct{} +type JapanFruitFactory struct{} +type AmericaFruitFactory struct{} + +func (china *ChinaFruitFactory) CreateApple() Apple { + apple := new(ChinaApple) + return apple +} +func (china *ChinaFruitFactory) CreateBanana() Banana { + banana := new(ChinaBanana) + return banana +} +func (china *ChinaFruitFactory) CreatePear() Pear { + pear := new(ChinaPear) + return pear +} + +func (japan *JapanFruitFactory) CreateApple() Apple { + apple := new(JapanApple) + return apple +} +func (japan *JapanFruitFactory) CreateBanana() Banana { + banana := new(JapanBanana) + return banana +} +func (japan *JapanFruitFactory) CreatePear() Pear { + pear := new(JapanPear) + return pear +} + +func (america *AmericaFruitFactory) CreateApple() Apple { + apple := new(AmericaApple) + return apple +} +func (america *AmericaFruitFactory) CreateBanana() Banana { + banana := new(AmericaBanana) + return banana +} +func (america *AmericaFruitFactory) CreatePear() Pear { + pear := new(AmericaPear) + return pear +} + +type Apple interface{ ShowApple() } +type Banana interface{ ShowBanana() } +type Pear interface{ ShowPear() } +type ChinaApple struct{} +type ChinaBanana struct{} +type ChinaPear struct{} +type JapanApple struct{} +type JapanBanana struct{} +type JapanPear struct{} +type AmericaApple struct{} +type AmericaBanana struct{} +type AmericaPear struct{} + +func (chinaApple *ChinaApple) ShowApple() { + fmt.Println("我是中国苹果") +} +func (chinaBanana *ChinaBanana) ShowBanana() { + fmt.Println("我是中国香蕉") +} +func (chinaPear *ChinaPear) ShowPear() { + fmt.Println("我是中国梨") +} +func (japanApple *JapanApple) ShowApple() { + fmt.Println("我是日本苹果") +} +func (japanBanana *JapanBanana) ShowBanana() { + fmt.Println("我是日本香蕉") +} +func (japanPear *JapanPear) ShowPear() { + fmt.Println("我是日本梨") +} +func (americaApple *AmericaApple) ShowApple() { + fmt.Println("我是美国苹果") +} +func (americaBanana *AmericaBanana) ShowBanana() { + fmt.Println("我是美国香蕉") +} +func (americaPear *AmericaPear) ShowPear() { + fmt.Println("我是美国梨") +} +func main() { + chinaFactory := &ChinaFruitFactory{} + japanFactory := &JapanFruitFactory{} + americaFactory := &AmericaFruitFactory{} + + chinaApple := chinaFactory.CreateApple() + chinaApple.ShowApple() + + japanBanana := japanFactory.CreateBanana() + japanBanana.ShowBanana() + + americaPear := americaFactory.CreatePear() + americaPear.ShowPear() +} +``` + +这段代码是使用Go语言实现的一个抽象工厂模式的例子。在这个例子中,我们定义了一个`FruitFactory`​接口,该接口声明了三个方法:`CreateApple()`​, `CreateBanana()`​, 和 `CreatePear()`​。每个方法返回特定类型的水果实例。 + +具体实现的工厂有`ChinaFruitFactory`​, `JapanFruitFactory`​, 和 `AmericaFruitFactory`​,它们都实现了`FruitFactory`​接口,并分别创建各自国家的苹果、香蕉和梨。 + +每种水果都有一个接口定义(`Apple`​, `Banana`​, `Pear`​),以及具体的实现类(例如`ChinaApple`​, `JapanApple`​, `AmericaApple`​等)。这些具体的水果类实现了各自的展示方法,如`ShowApple()`​。 + +因此,从设计模式的角度来看,这段代码确实实现了抽象工厂模式。抽象工厂模式提供了一个接口,用于创建一系列相关或相互依赖的对象,而无需指定它们具体的类。在这个例子中,每个工厂可以创建一组相关的水果对象(苹果、香蕉和梨),并且可以根据需要选择不同的工厂来创建不同地区的产品。 + +为了完整这个示例并验证其功能,你可以添加一些调用代码到`main()`​函数中,比如创建一个工厂实例并使用它来生成各种水果对象。下面是一个简单的示例: + +这样的代码将会输出每个工厂创建的水果的信息。 + +### 2.1.5 建造者模式 + +![](https://blog.meowrain.cn/api/i/2024/11/18/rVJ1qt1731921499773628860.webp)​ + +建造者模式(Builder Pattern),又叫生成器模式,将复杂对象的创建过程和对象本身进行抽象和分离,使得创建过程可以重用并创建多个不同表现的对象。 + +优点: + +1. 各个具体的建造者相互独立,有利于系统的扩展; + +1. 客户端不必知道产品内部组成的细节,便于控制细节风险。 + +缺点:1. 产品的创建过程相同才能重用,有一定的局限性; + +1. 产品的内部创建过程变化。 + +#### 建造者模式结构 + +建造者模式有4个角色: + +1. 产品角色(Product):由具体建造者来实现各个创建步骤的业务逻辑; +2. 抽象建造者(Abstract Builder):可以是接口和抽象类,通常会定义一个方法来创建复杂产品,一般命名为`build`​; +3. 具体建造者(Concrete Builder):实现抽象建造者中定义的创建步骤逻辑,完成复杂产品的各个部件的具体创建方法 +4. 指挥者(Director):调用抽象建造者对象中的部件构造与组装方法,然后调用`build`​方法完成复杂对象的创建; + +![](https://blog.meowrain.cn/api/i/2024/11/18/YeNVRi1731922084010556491.webp)​ + +Director内部组合了抽象的Builder,用来构建产品;Client依赖Director创建产品,但是需要告诉它使用什么具体的Builder来创建,也就是会依赖具体的构建器. + +抽象的Builder有两种形式:抽象类和接口。图中抽象的Builder为抽象类,其内部组合依赖了Product,并在build方法直接返回它;如果抽象Builder为接口,那么内部不会依赖Product,类结构上也会有一些变化,如下图所示: + +![](https://blog.meowrain.cn/api/i/2024/11/18/sHBlTj1731922203195022589.webp)​ + +最显著的区别是,具体构建器需要实现build方法,返回具产品信息。 + +```go +package main + +import "fmt" + +type Car struct { + //发动机 + motor string + //变速箱 + gearbox string + //底盘 + chassis string + // 车架 + frame string +} + +type Builder interface { + BuildMotor() Builder + BuildGearbox() Builder + BuildChassis() Builder + BuildFrame() Builder + Build() *Car +} + +type CarBuilder struct { + car Car +} + +func (c *CarBuilder) BuildMotor() Builder { + c.car.motor = "Motor" + return c +} +func (c *CarBuilder) BuildGearbox() Builder { + c.car.gearbox = "Gearbox" + return c +} +func (c *CarBuilder) BuildChassis() Builder { + c.car.chassis = "Chassis" + return c +} +func (c *CarBuilder) BuildFrame() Builder { + return c +} +func (c *CarBuilder) Build() *Car { + return &c.car +} + +type Director struct { + builder Builder +} + +func (d *Director) Construct() *Car { + d.builder.BuildFrame().BuildChassis().BuildMotor().BuildGearbox() + return d.builder.Build() +} +func main() { + var director Director = Director{ + builder: new(CarBuilder), + } + car := director.Construct() + fmt.Printf("Car built with: %s, %s, %s, %s\n", car.frame, car.chassis, car.motor, car.gearbox) +} +``` + +建造者模式(Builder Pattern)在实际生产中有很多应用场景,尤其适用于构建复杂对象。这些对象通常由多个部分组成,并且这些部分可能具有不同的配置和创建顺序。以下是几个实际生产中使用建造者模式的例子,并附有Go的代码示例 + +#### 配置复杂对象 + +在Web应用程序中,配置复杂的请求或响应对象是一个常见的需求。建造者模式可以用于构建配置复杂的HTTP请求。 + +```go +package main + +import ( + "bytes" + "fmt" + "io" + "net/http" + "os" + "time" +) + +type HTTPRequestBuilder interface { + SetMethod(method string) HTTPRequestBuilder + SetURL(url string) HTTPRequestBuilder + SetHeader(headers map[string]string) HTTPRequestBuilder + SetBody(body []byte) HTTPRequestBuilder + SetTimeout(duration time.Duration) HTTPRequestBuilder + Build() (*http.Request, error) +} + +type ConcreteHTTPRequestBuilder struct { + method string + url string + headers map[string]string + body []byte + timeout time.Duration +} + +func (b *ConcreteHTTPRequestBuilder) SetMethod(method string) HTTPRequestBuilder { + validMethods := map[string]bool{ + "GET": true, + "POST": true, + "PUT": true, + "DELETE": true, + "PATCH": true, + "OPTIONS": true, + "HEAD": true, + } + // 验证 HTTP 方法是否合法 + if !validMethods[method] { + panic("Invalid HTTP method: " + method) + } + b.method = method + return b +} + +func (b *ConcreteHTTPRequestBuilder) SetURL(url string) HTTPRequestBuilder { + if url == "" { + panic("URL cannot be empty") + } + b.url = url + return b +} + +func (b *ConcreteHTTPRequestBuilder) SetHeader(headers map[string]string) HTTPRequestBuilder { + if headers == nil { + headers = make(map[string]string) + } + b.headers = headers + return b +} + +func (b *ConcreteHTTPRequestBuilder) SetBody(body []byte) HTTPRequestBuilder { + b.body = body + return b +} + +func (b *ConcreteHTTPRequestBuilder) SetTimeout(duration time.Duration) HTTPRequestBuilder { + b.timeout = duration + return b +} + +func (b *ConcreteHTTPRequestBuilder) Build() (*http.Request, error) { + // 构建 HTTP 请求,并将 body 转换为 io.Reader 类型 + req, err := http.NewRequest(b.method, b.url, bytes.NewBuffer(b.body)) + if err != nil { + return nil, err + } + // 设置请求头 + for k, v := range b.headers { + req.Header.Set(k, v) + } + return req, nil +} + +type HTTPRequestDirector struct { + builder HTTPRequestBuilder + timeout time.Duration +} + +func (d *HTTPRequestDirector) NewHTTPRequestBuilder(method string, url string, header map[string]string, body []byte) (*http.Request, error) { + req, err := d.builder.SetMethod(method).SetURL(url).SetHeader(header).SetBody(body).Build() + if err != nil { + return nil, err + } + return req, nil +} + +func (d *HTTPRequestDirector) DoRequest(req *http.Request) (*http.Response, string, error) { + client := &http.Client{Timeout: d.timeout} + resp, err := client.Do(req) + if err != nil { + return nil, "", err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, "", err + } + return resp, string(body), nil +} + +func main() { + // 创建 HTTP 请求的指导者 + director := HTTPRequestDirector{ + builder: &ConcreteHTTPRequestBuilder{}, + timeout: 5 * time.Second, + } + + // 构建一个 POST 请求,带上请求体 + body := []byte(`{"name":"John","age":30}`) + headers := map[string]string{ + "Content-Type": "application/json", + } + req, err := director.NewHTTPRequestBuilder("POST", "https://httpbin.org/post", headers, body) + if err != nil { + fmt.Printf("Error creating request: %v\n", err) + os.Exit(1) + } + + // 发送请求并获取响应 + resp, bodyStr, err := director.DoRequest(req) + if err != nil { + fmt.Printf("Error making request: %v\n", err) + os.Exit(1) + } + + // 打印响应状态和响应体 + fmt.Printf("Response status: %s\n", resp.Status) + fmt.Printf("Response body: %s\n", bodyStr) +} +``` + +![](https://blog.meowrain.cn/api/i/2024/11/18/R5WFTq1731925462907647500.webp)​ + +### 2.1.6 原型模式 + +![](https://blog.meowrain.cn/api/i/2024/11/18/Psd0gk1731925545967851391.webp)​ + +原型模式(Prototype Pattern),它的基本思想是:创建一个对象实例作为原型,然后不断的**复制**(或者叫克隆)这个原型对象来创建该对象的新实例,而不是反复的使用构造函数来实例化对象。 + +原型模式创建对象,调用者无需关心对象创建细节,只需要调用复制方法,即可得到与原型对象属性相同的新实例,方便而且高效。 + +原型模式的结构如下图所示: + +![](https://blog.meowrain.cn/api/i/2024/11/18/djesfV1731925807312375868.webp)​ + +#### 浅拷贝和深拷贝 + +##### 基本类型 + +基本类型是 Go 语言自带的类型,比如 数值、浮点、字符串、布尔、数组 及 错误 类型,他们本质上是原始类型,也就是不可改变的,所以对他们进行操作,一般都会返回一个新创建的值,所以把这些值传递给函数时,其实传递的是一个值的副本。 + +##### 引用类型 + +引用类型和原始的基本类型恰恰相反,它的修改可以影响到任何引用到它的变量。在 Go 语言中,引用类型有 切片(slice)、字典(map)、接口(interface)、函数(func) 以及 通道(chan) 。 + +引用类型之所以可以引用,是因为我们创建引用类型的变量,其实是一个标头值,标头值里包含一个指针,指向底层的数据结构,当我们在函数中传递引用类型时,其实传递的是这个标头值的副本,它所指向的底层结构并没有被复制传递,这也是引用类型传递高效的原因。 + +本质上,我们可以理解函数的传递都是值传递,只不过引用类型传递的是一个指向底层数据的指针,所以我们在操作的时候,可以修改共享的底层数据的值,进而影响到所有引用到这个共享底层数据的变量。 + + + +--- + +浅拷贝是指创建一个新对象,这个新对象拥有原对象的部分属性值。对于值类型的数据,直接复制其值;对于引用类型的数据,只复制引用,不复制引用的对象本身。这意味着浅拷贝后的新对象和原对象共享相同的引用对象。 + +‍ + +深拷贝是指创建一个新对象,并且递归地复制所有引用对象。这意味着深拷贝后的新对象和原对象完全独立,修改新对象不会影响原对象。 + +--- + +##### **结构体的拷贝行为取决于字段的类型** + +当你直接复制结构体时,Go 会逐字段进行值拷贝,对于值类型字段(例如 `int`​、`string`​、`float`​ 等),它们会被独立复制,而对于引用类型字段(如切片、映射、指针等),它们会被拷贝为引用,即会指向同一个底层数据。 + +深拷贝意味着不仅仅是结构体本身的拷贝,还需要对结构体中所有的引用类型字段(如切片、映射、指针等)进行递归拷贝,以确保每个引用类型字段的底层数据都被独立复制。 + +--- + +‍ + +我们先用浅拷贝简单实现一下原型模式 + +> +> +> **Golang基本类型和引用类型** + +```go +package main + +import ( + "fmt" + "unsafe" +) + +// Document 接口定义了克隆方法和获取信息方法 +type Document interface { + Clone() Document + GetInfo() string +} + +// TextDocument 是一个包含引用类型的文档结构体 +type TextDocument struct { + Content string + Tags []string +} + +// Clone 方法实现浅拷贝 +func (t *TextDocument) Clone() Document { + clon := *t + return &clon +} + +// GetInfo 方法返回文档内容 +func (t *TextDocument) GetInfo() string { + return t.Content +} + +func main() { + textDoc := &TextDocument{ + Content: "Hello World!", + Tags: []string{"golang", "programming"}, + } + + // 克隆文档 + clonedDoc := textDoc.Clone().(*TextDocument) + + fmt.Printf("Original Document Pointer: %p\n", &textDoc) + fmt.Printf("Cloned Document Pointer: %p\n", &clonedDoc) + fmt.Println() + // 修改克隆对象的 Tags + clonedDoc.Tags[0] = "cloned" + + fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [cloned programming] + fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] + + // 检查 Tags 切片的底层数组地址 + /* + 浅拷贝和深拷贝的一些微妙之处。在Go中,当我们进行浅拷贝时,如果结构体包含了切片(引用类型), + 新的结构体和原始结构体的切片字段将引用同一个底层数组。尽管切片本身的地址不同,它们指向的底层数组是相同的。 + */ + fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) + fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) + fmt.Println() + fmt.Printf("Original Document Tags Array Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) + fmt.Printf("Cloned Document Tags Array Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +} + +``` + +```go +package main + +import ( + "fmt" + "unsafe" +) + +// Document 接口定义了克隆方法和获取信息方法 +type Document interface { + Clone() Document + GetInfo() string +} + +// TextDocument 是一个包含引用类型的文档结构体 +type TextDocument struct { + Content string + Tags []string +} + +// Clone 方法实现浅拷贝 +func (t *TextDocument) Clone() Document { + clon := *t + return &clon +} + +// GetInfo 方法返回文档内容 +func (t *TextDocument) GetInfo() string { + return t.Content +} + +func main() { + textDoc := &TextDocument{ + Content: "Hello World!", + Tags: []string{"golang", "programming"}, + } + + // 克隆文档 + clonedDoc := textDoc.Clone().(*TextDocument) + + fmt.Printf("Original Document Pointer: %p\n", &textDoc) + fmt.Printf("Cloned Document Pointer: %p\n", &clonedDoc) + fmt.Println() + // 修改克隆对象的 Tags + clonedDoc.Tags[0] = "cloned" + + fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [cloned programming] + fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] + + // 检查 Tags 切片的底层数组地址 + /* + 浅拷贝和深拷贝的一些微妙之处。在Go中,当我们进行浅拷贝时,如果结构体包含了切片(引用类型), + 新的结构体和原始结构体的切片字段将引用同一个底层数组。尽管切片本身的地址不同,它们指向的底层数组是相同的。 + */ + fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) + fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) + fmt.Println() + fmt.Printf("Original Document Tags Array Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) + fmt.Printf("Cloned Document Tags Array Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +} + +``` + +![](https://blog.meowrain.cn/api/i/2024/11/18/bgwZCc1731927529364500874.webp)​ + +根据输出结果我们能看到 + +> **其实在golang中,浅拷贝我们没必要去写这个clone函数,因为直接赋值就已经实现了浅拷贝的效果** +> +> **直接赋值和浅拷贝的区别** +> +> **直接赋值:** +> +> * 当你直接赋值一个结构体时,Go会对所有字段进行浅拷贝。 +> * 对于引用类型(如切片、指针、映射等),直接赋值会复制引用,所以原对象和新对象将共享相同的底层数据。 +> +> **使用** `Clone`​ **方法的浅拷贝:** +> +> * 使用 `Clone`​ 方法进行浅拷贝时,如果你在方法内部对结构体进行赋值操作(即 `clon := *t`​),Go会创建一个新结构体,但所有引用类型的字段仍然共享相同的底层数据。 +> +> 可以在下面的代码中得到验证 +> +> ![](https://blog.meowrain.cn/api/i/2024/11/18/J0b9iY1731928162025069696.webp)​ +> +> ```go +> package main +> +> import ( +> "fmt" +> "unsafe" +> ) +> +> // Document 接口定义了克隆方法和获取信息方法 +> type Document interface { +> Clone() Document +> GetInfo() string +> } +> +> // TextDocument 是一个包含引用类型的文档结构体 +> type TextDocument struct { +> Content string +> Tags []string +> } +> +> // Clone 方法实现浅拷贝 +> func (t *TextDocument) Clone() Document { +> clon := *t +> return &clon +> } +> +> // GetInfo 方法返回文档内容 +> func (t *TextDocument) GetInfo() string { +> return t.Content +> } +> +> func main() { +> textDoc := &TextDocument{ +> Content: "Hello World!", +> Tags: []string{"golang", "programming"}, +> } +> +> // 克隆文档 +> //clonedDoc := textDoc.Clone().(*TextDocument) +> clonedDoc := textDoc +> fmt.Printf("Original Document Pointer: %p\n", &textDoc) +> fmt.Printf("Cloned Document Pointer: %p\n", &clonedDoc) +> fmt.Println() +> // 修改克隆对象的 Tags +> clonedDoc.Tags[0] = "cloned" +> +> fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [cloned programming] +> fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] +> +> // 检查 Tags 切片的底层数组地址 +> /* +> 浅拷贝和深拷贝的一些微妙之处。在Go中,当我们进行浅拷贝时,如果结构体包含了切片(引用类型), +> 新的结构体和原始结构体的切片字段将引用同一个底层数组。尽管切片本身的地址不同,它们指向的底层数组是相同的。 +> */ +> fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) +> fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) +> fmt.Println() +> fmt.Printf("Original Document Tags Array Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) +> fmt.Printf("Cloned Document Tags Array Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +> +> } +> +> ``` + +深拷贝 + +深拷贝的两种方法: + +1. 序列化 +2. 反射 + +下面我们来分开讲这两种方式 + +序列化 + +```go +package main + +import ( + "bytes" + "encoding/gob" + "fmt" + "unsafe" +) + +// Document 接口定义了克隆方法和获取信息方法 +type Document interface { + Clone() Document + GetInfo() string +} + +// TextDocument 是一个包含引用类型的文档结构体 +type TextDocument struct { + Content string + Tags []string +} + +// Clone 方法实现深拷贝 +func (t *TextDocument) Clone() Document { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + dec := gob.NewDecoder(&buf) + + // 编码原始对象 + if err := enc.Encode(t); err != nil { + panic(err) + } + + // 解码到新对象 + cloned := &TextDocument{} + if err := dec.Decode(cloned); err != nil { + panic(err) + } + + return cloned +} + +// GetInfo 方法返回文档内容 +func (t *TextDocument) GetInfo() string { + return t.Content +} + +func main() { + textDoc := &TextDocument{ + Content: "Hello World!", + Tags: []string{"golang", "programming"}, + } + + // 克隆文档 + clonedDoc := textDoc.Clone().(*TextDocument) + + fmt.Printf("Original Document Pointer: %p\n", &textDoc) + fmt.Printf("Cloned Document Pointer: %p\n", &clonedDoc) + fmt.Println() + // 修改克隆对象的 Tags + clonedDoc.Tags[0] = "cloned" + + fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [golang programming] + fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] + + // 检查 Tags 切片的底层数组地址 + fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) + fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) + fmt.Println() + fmt.Printf("Original Document Tags Slice Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) + fmt.Printf("Cloned Document Tags Slice Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +} + + +``` + +![](https://blog.meowrain.cn/api/i/2024/11/19/OfQAxD1732016103050848862.webp)​ + +其实我们还可以单独把slice复制一份新的重新赋值 + +```go +package main + +import ( + "fmt" + "unsafe" +) + +// Document 接口定义了克隆方法和获取信息方法 +type Document interface { + Clone() Document + GetInfo() string +} + +// TextDocument 是一个包含引用类型的文档结构体 +type TextDocument struct { + Content string + Tags []string +} + +// Clone 方法实现深拷贝 +func (t *TextDocument) Clone() Document { + newSlice := make([]string, len(t.Tags)) + copy(newSlice, t.Tags) + clon := *t + clon.Tags = newSlice + return &clon +} + +// GetInfo 方法返回文档内容 +func (t *TextDocument) GetInfo() string { + return t.Content +} + +func main() { + textDoc := &TextDocument{ + Content: "Hello World!", + Tags: []string{"golang", "programming"}, + } + + // 克隆文档 + clonedDoc := textDoc.Clone().(*TextDocument) + + fmt.Printf("Original Document Pointer: %p\n", textDoc) + fmt.Printf("Cloned Document Pointer: %p\n", clonedDoc) + fmt.Println() + // 修改克隆对象的 Tags + clonedDoc.Tags[0] = "cloned" + + fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [golang programming] + fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] + + // 检查 Tags 切片的底层数组地址 + fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) + fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) + fmt.Println() + fmt.Printf("Original Document Tags Array Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) + fmt.Printf("Cloned Document Tags Array Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +} + +``` + +反射法 + +```go +package main + +import ( + "fmt" + "unsafe" +) + +// Document 接口定义了克隆方法和获取信息方法 +type Document interface { + Clone() Document + GetInfo() string +} + +// TextDocument 是一个包含引用类型的文档结构体 +type TextDocument struct { + Content string + Tags []string +} + +// Clone 方法实现深拷贝 +func (t *TextDocument) Clone() Document { + newSlice := make([]string, len(t.Tags)) + copy(newSlice, t.Tags) + clon := *t + clon.Tags = newSlice + return &clon +} + +// GetInfo 方法返回文档内容 +func (t *TextDocument) GetInfo() string { + return t.Content +} + +func main() { + textDoc := &TextDocument{ + Content: "Hello World!", + Tags: []string{"golang", "programming"}, + } + + // 克隆文档 + clonedDoc := textDoc.Clone().(*TextDocument) + + fmt.Printf("Original Document Pointer: %p\n", textDoc) + fmt.Printf("Cloned Document Pointer: %p\n", clonedDoc) + fmt.Println() + // 修改克隆对象的 Tags + clonedDoc.Tags[0] = "cloned" + + fmt.Println("Original Document Tags:", textDoc.Tags) // 输出: [golang programming] + fmt.Println("Cloned Document Tags:", clonedDoc.Tags) // 输出: [cloned programming] + + // 检查 Tags 切片的底层数组地址 + fmt.Printf("Original Document Tags Pointer: %p\n", &textDoc.Tags) + fmt.Printf("Original Document Tags Pointer: %p\n", &clonedDoc.Tags) + fmt.Println() + fmt.Printf("Original Document Tags Array Pointer: %p\n", unsafe.Pointer(&textDoc.Tags[0])) + fmt.Printf("Cloned Document Tags Array Pointer: %p\n", unsafe.Pointer(&clonedDoc.Tags[0])) +} + +``` + +## 2.2 结构型模式 + +### 2.2.1 代理模式 + + Proxy模式又叫做代理模式,是构造型的设计模式之一,它可以为其他对象提供一种代理(Proxy)以控制对这个对象的访问。 + 所谓代理,是指具有与代理元(被代理的对象)具有相同的接口的类,客户端必须通过代理与被代理的目标类交互,而代理一般在交互的过程中(交互前后),进行某些特别的处理。 + +用一个日常可见的案例来理解“代理”的概念,如下图: + +代理模式中的角色和职责 +​![image](https://blog.meowrain.cn/api/i/2024/08/06/RdShdI1722952015053702620.webp)​ + +代理模式案例实现 +​![image](https://blog.meowrain.cn/api/i/2024/08/06/72QXJ71722952087937330915.webp)​ + +代码如下 + +```go +package proxy + +import "fmt" + +type Buy interface { + Buy() +} + +type BuyProxy struct { + buyer Buy +} + +func (buy BuyProxy) PreBuy() { + fmt.Println("pre buy something") +} + +func (buy BuyProxy) Buy() { + buy.PreBuy() + buy.buyer.Buy() + buy.PostBuy() +} + +func (buy BuyProxy) PostBuy() { + fmt.Println("post buy something") +} + +type BuyFromChina struct { +} + +func (buy BuyFromChina) Buy() { + // buy something from china + fmt.Println("buy something from china") +} + +type BuyFromAmerica struct { +} + +func (buy BuyFromAmerica) Buy() { + fmt.Println("buy something from america") +} + +type BuyFromJapan struct { +} + +func (buy BuyFromJapan) Buy() { + fmt.Println("buy something from japan") +} + +func BuyTest() { + buy := BuyProxy{buyer: BuyFromChina{}} + buy.Buy() +} + +``` + +代理模式的优缺点 +优点: +(1) 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度。 +(2) 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性。 + +缺点: +(1) 代理实现较为复杂。 + +### 2.2.2 装饰模式 + + 装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。 + +![image](https://blog.meowrain.cn/api/i/2024/08/06/R6WToE1722952827310566912.webp)​ + +装饰模式中的角色和职责 +Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。 +ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。 + +![image](https://blog.meowrain.cn/api/i/2024/08/06/Bcyxbt1722952917869251667.webp)​ + +装饰模式中的代码实现 +​![image](https://blog.meowrain.cn/api/i/2024/08/06/WHFIL71722953108594578446.webp)​ + +```go +package decorator + +import "fmt" + +type Phone interface { + Show() +} +type HuaweiPhone struct{} + +func (h HuaweiPhone) Show() { + fmt.Println("This is a Huawei phone") +} + +type XiaomiPhone struct{} + +func (xh XiaomiPhone) Show() { + fmt.Println("This is a Xiaomi phone") +} + +type Decorator interface { + Show() +} + +type AddScreenProtectionDecorator struct { + Phone +} + +func (a AddScreenProtectionDecorator) Show() { + fmt.Println("Add screen protection") + a.Phone.Show() +} + +type AddShellProtectionDecorator struct { + Phone +} + +func (a AddShellProtectionDecorator) Show() { + fmt.Println("Add shell protection") + a.Phone.Show() +} +func DecorateTest() { + + huaweiWithScreenProtection := AddScreenProtectionDecorator{HuaweiPhone{}} + huaweiWithScreenProtection.Show() + + xiaomiWithShellProtection := AddShellProtectionDecorator{XiaomiPhone{}} + xiaomiWithShellProtection.Show() +} + +``` + +装饰模式: +优点: +(1) 对于扩展一个对象的功能,装饰模式比继承更加灵活性,不会导致类的个数急剧增加。 +(2) 可以通过一种动态的方式来扩展一个对象的功能,从而实现不同的行为。 +(3) 可以对一个对象进行多次装饰。 +(4) 具体构件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体构件类和具体装饰类,原有类库代码无须改变,符合“开闭原则”。 +缺点: +(1) 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,影响程序的性能。 +(2) 装饰模式提供了一种比继承更加灵活机动的解决方案,但同时也意味着比继承更加易于出错,排错也很困难,对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为繁琐。 + +装饰模式(Decorator Pattern)和代理模式(Proxy Pattern)都是结构型设计模式,但它们的目的和应用场景有所不同。下面是对这两种模式的简要说明以及它们之间的区别: + +##### 装饰模式 + +* **目的**:动态地给一个对象添加一些额外的职责。装饰模式提供了一种比继承更具弹性的替代方案。 +* **适用场景**: + + * 当需要扩展一个类的功能或给一个类添加附加职责时。 + * 当不能采用生成子类的方法进行扩展时,一种情况是可能有大量独立的扩展,为每一种组合将产生大量的子类,使得子类数目呈爆炸性增长。 +* **结构**: + + * 包含一个抽象组件(Component)接口。 + * 具体组件(Concrete Component)实现抽象组件接口。 + * 抽象装饰器(Decorator)作为抽象组件的子类,持有具体组件的引用。 + * 具体装饰器(Concrete Decorators)实现抽象装饰器,并添加职责。 +* **特点**: + + * 动态性:可以在运行时动态地增加功能,也可以移除已有的功能。 + * 透明性:客户端不需要知道具体的装饰器,只需与抽象组件交互即可。 + * 复用性:可以创建多个装饰器,每个装饰器可以单独使用,也可以组合使用。 + +##### 代理模式 + +* **目的**:为另一个对象提供一个代理以控制对这个对象的访问。代理对象可以拦截客户端对真实对象的访问,做一些额外的处理。 +* **适用场景**: + + * 远程代理(Remote Proxy):为远程对象提供本地代理。 + * 虚拟代理(Virtual Proxy):当对象创建开销很大时,先创建一个代理对象,等到真正需要的时候再创建真实对象。 + * 保护代理(Protection Proxy):控制对真实对象的访问权限。 +* **结构**: + + * 包含一个主题(Subject)接口。 + * 具体主题(Real Subject)实现主题接口。 + * 代理(Proxy)也实现主题接口,并持有具体主题的引用。 +* **特点**: + + * 透明性:客户端可以像对待真实对象一样对待代理对象。 + * 控制访问:代理可以控制对真实对象的访问,比如缓存、权限验证等。 + * 间接性:代理模式允许在客户端和真实对象之间建立间接关系。 + +### 不同之处 + +* **目的**:装饰模式用于动态地给对象添加职责;代理模式用于控制对对象的访问。 +* **使用场景**:装饰模式适用于扩展对象的功能;代理模式适用于控制或优化对象的访问。 +* **实现方式**:装饰模式通过装饰器类来扩展功能;代理模式通过代理类来控制访问。 +* **结构差异**:装饰模式中的装饰器持有具体组件的实例;代理模式中的代理持有具体主题的实例。 + +总结来说,装饰模式关注的是对象的扩展,而代理模式关注的是对象的访问控制。在实际应用中,可以根据需要选择合适的模式来解决问题。 + +### 2.2.3 适配器模式 + + 将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 + +适配器模式中的角色和职责 +​![image](https://blog.meowrain.cn/api/i/2024/08/06/KTGuAn1722953697734933933.webp)​ + +Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。 + +Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承Target并关联一个Adaptee对象使二者产生联系。 + +Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。 + +根据对象适配器模式结构图,在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specificRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。 + +代码实现 + +当然可以!让我们通过另一个例子来更好地理解适配器模式。这次我们将创建一个更简单的例子,涉及到一个天气预报系统,该系统需要与两种不同的温度传感器接口进行交互:一种是老式的摄氏温度传感器(CelsiusSensor),另一种是新式的华氏温度传感器(FahrenheitSensor)。 + +我们的目标是创建一个适配器,使我们可以使用相同的接口从这两种不同的传感器获取温度读数。 + +##### 示例代码 + +```go +package main + +import ( + "fmt" +) + +// 定义老式摄氏温度传感器接口 +type CelsiusSensor interface { + GetCelsiusTemperature() float64 +} + +// 定义新式华氏温度传感器接口 +type FahrenheitSensor interface { + GetFahrenheitTemperature() float64 +} + +// 实现老式摄氏温度传感器 +type OldCelsiusSensor struct{} + +func (ocs *OldCelsiusSensor) GetCelsiusTemperature() float64 { + return 25.0 // 假设这是从老式传感器获得的温度 +} + +// 实现新式华氏温度传感器 +type NewFahrenheitSensor struct{} + +func (nfs *NewFahrenheitSensor) GetFahrenheitTemperature() float64 { + return 77.0 // 假设这是从新式传感器获得的温度 +} + +// 定义温度传感器适配器接口 +type TemperatureSensor interface { + GetTemperature() float64 +} + +// 实现温度传感器适配器 +type TemperatureSensorAdapter struct { + celsiusSensor CelsiusSensor + fahrenheitSensor FahrenheitSensor +} + +func (tsa *TemperatureSensorAdapter) GetTemperature() float64 { + if tsa.celsiusSensor != nil { + return tsa.celsiusSensor.GetCelsiusTemperature() + } else if tsa.fahrenheitSensor != nil { + return (tsa.fahrenheitSensor.GetFahrenheitTemperature() - 32) * 5 / 9 + } + return 0.0 +} + +func main() { + // 创建老式摄氏温度传感器实例 + oldCelsiusSensor := &OldCelsiusSensor{} + + // 创建新式华氏温度传感器实例 + newFahrenheitSensor := &NewFahrenheitSensor{} + + // 创建适配器 + adapterForCelsius := &TemperatureSensorAdapter{celsiusSensor: oldCelsiusSensor} + adapterForFahrenheit := &TemperatureSensorAdapter{fahrenheitSensor: newFahrenheitSensor} + + // 获取温度 + fmt.Println("Temperature from old Celsius sensor:", adapterForCelsius.GetTemperature(), "°C") + fmt.Println("Temperature from new Fahrenheit sensor:", adapterForFahrenheit.GetTemperature(), "°C") +} +``` + +##### 解释 + +1. **CelsiusSensor** 接口定义了一个方法 `GetCelsiusTemperature()`​,用于获取摄氏温度。 +2. **FahrenheitSensor** 接口定义了一个方法 `GetFahrenheitTemperature()`​,用于获取华氏温度。 +3. **OldCelsiusSensor** 类实现了 `CelsiusSensor`​ 接口,返回一个固定的摄氏温度值。 +4. **NewFahrenheitSensor** 类实现了 `FahrenheitSensor`​ 接口,返回一个固定的华氏温度值。 +5. **TemperatureSensor** 接口定义了一个方法 `GetTemperature()`​,用于获取温度,无论是摄氏还是华氏。 +6. **TemperatureSensorAdapter** 类实现了 `TemperatureSensor`​ 接口,并持有 `CelsiusSensor`​ 或 `FahrenheitSensor`​ 的引用。如果存在摄氏温度传感器,则直接返回摄氏温度;如果存在华氏温度传感器,则将其转换为摄氏温度并返回。 + +在 `main`​ 函数中,我们创建了两个传感器的实例以及相应的适配器,并调用了 `GetTemperature()`​ 方法来获取温度。 + +这个例子应该更容易理解一些,因为它涉及的是更常见的温度单位转换问题。 + +优缺点 +优点: +(1) 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无须修改原有结构。 +(2) 增加了类的透明性和复用性,将具体的业务实现过程封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用。 +(3) 灵活性和扩展性都非常好,可以很方便地更换适配器,也可以在不修改原有代码的基础上增加新的适配器类,完全符合“开闭原则”。 +缺点: +适配器中置换适配者类的某些方法比较麻烦。 + +### 2.2.4 外观模式 + +根据迪米特法则,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。 + +Facade模式也叫外观模式,是由GoF提出的23种设计模式中的一种。Facade模式为一组具有类似功能的类群,比如类库,子系统等等,提供一个一致的简单的界面。这个一致的简单的界面被称作facade。 + +#### 外观模式中角色和职责 + +![image](https://blog.meowrain.cn/api/i/2024/08/07/XiJHKE1722993185818102441.webp)​ + +Façade(外观角色):为调用方, 定义简单的调用接口。 +SubSystem(子系统角色):功能提供者。指提供功能的类群(模块或子系统)。 + +#### 外观模式的案例 + +![image](https://blog.meowrain.cn/api/i/2024/08/07/Ic15LO1722993673517689175.webp)​ + +```go +package appearance + +import "fmt" + +// 家庭影院(外观) +type HomePlayerFacade struct { + tv TV //电视 + mp MicroPhone //麦克风 + light Light //灯光 + speaker Speaker //扬声器 + xbox GameConsole //游戏机 + pro Projector //投影仪 +} + +type Switch interface { + On() + Off() +} + +type TV struct{} +type GameConsole struct{} +type Light struct{} +type MicroPhone struct{} +type Speaker struct{} +type Projector struct{} + +func (t *TV) On() { + fmt.Println("TV is on") +} +func (t *TV) Off() { + fmt.Println("TV is off") +} +func (g *GameConsole) On() { + fmt.Println("GameConsole is on") +} +func (g *GameConsole) Off() { + fmt.Println("GameConsole is off") +} +func (l *Light) On() { + fmt.Println("Light is on") +} +func (l *Light) Off() { + fmt.Println("Light is off") +} + +func (m *MicroPhone) On() { + fmt.Println("MicroPhone is on") +} +func (m *MicroPhone) Off() { + fmt.Println("MicroPhone is off") +} +func (s *Speaker) On() { + fmt.Println("Speaker is on") +} +func (s *Speaker) Off() { + fmt.Println("Speaker is off") +} +func (p *Projector) On() { + fmt.Println("Projector is on") +} +func (p *Projector) Off() { + fmt.Println("Projector is off") +} + +// KTV MODE +func (homePlayer *HomePlayerFacade) KTVMode() { + fmt.Println("==================KTVMode===================") + homePlayer.tv.On() + homePlayer.mp.On() + homePlayer.light.Off() + homePlayer.speaker.On() + homePlayer.pro.On() + fmt.Println("=====================================") +} + +// Gaming MODE +func (homePlayer *HomePlayerFacade) GamingMode() { + fmt.Println("==================Gaming Mode===================") + homePlayer.tv.Off() + homePlayer.mp.Off() + homePlayer.light.Off() + homePlayer.speaker.On() + homePlayer.pro.On() + homePlayer.xbox.On() + fmt.Println("=====================================") +} +func AppearanceTest() { + homePlayer := HomePlayerFacade{} + homePlayer.GamingMode() + homePlayer.KTVMode() +} + +``` + +![image](https://blog.meowrain.cn/api/i/2024/08/07/MJz7KI1722994632265396771.webp) + +## 2.3 行为型模式 + +### 2.3.1 模板方法模式 + +### 2.3.2 命令模式 + +### 2.3.3 策略模式 + +### 2.3.4 观察者模式