Go语言实战:入门篇-5:函数、服务接口和Swagger UI
现在我们为用Go语言写一个Web服务已经做好了准备,不过在开始之前让我们往未来多想一步。相信使用Python+Swagger UI开发的各位已经被极其简单明了的可视化界面深深“套牢”了,我们也不希望在写好Go语言部署后只能通过苍白的路由地址,或者另起一个代码来测试最终的效果。因此在这里我们来为Go语言使用Swagger UI展示接口铺平道路。顺便验证一个迷你应用的落成吧。
一、函数
1. 匿名函数
2. 变长函数
3. 延迟函数调用
4. 宕机和恢复
5. 发现的小特点
(1)Go中不能像Python一样指明参数传参,所有参数都是顺序位置给出
(2)在使用结构体的时候凡是首字母没有大写的属性都不能被外部调用。而如果想在导出json时对此赋名的话需要用 `` 反引号写在后方
二、 Windows系统:
1. 新建一个项目目录用VS Code打开。然后运行初始化命令后安装所需的包
2. 整体项目展示:多接口整合
三、Linux系统:
一、函数
之前函数的学习中对函数的认知并不全面,更多地是从参数传递的方面介绍了。现在我们来看看函数本身有哪些神奇的应用:
1. 匿名函数
在之前的学习中我们明白了函数实际上就是一个胖指针,而指针是可以参与参数传递的,因此使用函数返回另一个函数也是理所应当的一个操作:
package mainimport "fmt"func squares() func() int {var x intreturn func() int {x++return x * x}
}func main() {x := squares()for i := 0; i <= 10; i++ {fmt.Println("调用x的值为:", x())}return
}调用x的值为: 1
调用x的值为: 4
调用x的值为: 9
调用x的值为: 16
调用x的值为: 25
调用x的值为: 36
调用x的值为: 49
调用x的值为: 64
调用x的值为: 81
调用x的值为: 100
调用x的值为: 121
2. 变长函数
同样对于参数来说,Python可以用*args和**kwags来接收变长的参数传入,Go中类似的方法尽管不像Python这么简单但是却很直白:
func sum(vals ...int) int {total := 0for _, val := range vals {total += val}return total
}func main() {fmt.Println(sum(54, 8, 46, 48, 13, 215, 5))return
}
同样如果需要获得更丰富的传入参数时可以使用interface{} 来实现:
func errorf(linenum int, format string, args ...interface{}) {fmt.Fprintf(os.Stderr, "Line %d:", linenum)fmt.Fprintf(os.Stderr, format, args...)fmt.Fprintln(os.Stderr)
}func main() {errorf(3, "aefoh", 156, 48, "alljei", "saohuf", 1.0, 0.5, sum)return
}Line 3:aefoh%!(EXTRA int=156, int=48, string=alljei, string=saohuf, float64=1, float64=0.5, func(...int) int=0x7ff7e0a306a0)
3. 延迟函数调用
这是一个在Python中我从来没有遇到的特性,语法上,它使用一个 defer 语句执行,无论在正常情况下执行 return 或者函数执行完毕,还是不正常情况下发生宕机,实际调用推迟到包含 defer 语句的函数结束后才执行。并且没有次数限制,执行的时候以调用 defer 语句的倒序进行。
它独有的特点决定了它最适合完成一些成对的任务比如连接/断开,启动/结束,加锁/解锁等操作。比如我们可以这样使用,来建立一个打开文件后正确关闭、获取锁执行完操作后正确解放的函数:
package ioutilimport ("os""sync"
)func ReadFile(filename string) ([]byte, error) {f, err := os.Open(filename)if err != nil {return nil, err}defer f.Close()return ReadAll(f)
}var mu sync.Mutex
var m = make(map[string][]int)func lookup(key string) int {mu.Lock()defer mu.Unlock()return m[key]
}
4. 宕机和恢复
Go语言的类型系统会捕捉很多编译时错误,可以将觉得部分错误拦截在运行之前。但是有的错误只能在运行中被发现,从而引发宕机。一个典型的宕机发生时,正常的程序执行会种种,goroutine中的所有延迟函数会被执行后,然后再异常退出留下一条日志消息。并不是所有的宕机都是在运行时发生,可以直接调用内置的宕机函数接收任意值。如果碰到“不可能发生”的情况,宕机是最好的处理:
package mainimport "fmt"func mayPanic() {fmt.Println("准备触发宕机...")panic("手动宕机:某个严重错误发生了!") // 手动触发 panicfmt.Println("这行不会被执行。")
}func safeRun() {// 使用 defer + recover 捕获 panicdefer func() {if r := recover(); r != nil {fmt.Println("已捕获到宕机:", r)}}()mayPanic()fmt.Println("从 mayPanic 返回。") // 不会执行
}func main() {fmt.Println("开始执行程序。")safeRun()fmt.Println("程序继续运行。")
}
5. 发现的小特点
(1)Go中不能像Python一样指明参数传参,所有参数都是顺序位置给出
在有一次我希望通过指定参数传递的时候意外地发现了报错,之后才发现原来Go中的函数只能进行默认地顺序传参,这个特性也潜在地规范了代码。
package mainimport "fmt"func greet(name string, age int, city string) string {return fmt.Sprintf("Halo, saya %s, usia %d, dari %s.", name, age, city)
}func main() {// 正确:按顺序传参fmt.Println(greet("Budi", 26, "Jakarta"))
}// func main() {
// // Go 不支持下面这种命名参数的调用 —— 这是非法的:
// // fmt.Println(greet(name: "Budi", age: 26, city: "Jakarta"))
// }
(2)在使用结构体的时候凡是首字母没有大写的属性都不能被外部调用。而如果想在导出json时对此赋名的话需要用 `` 反引号写在后方
这件事儿是我在调用API的时候发现的特点。我当时以为构造结构体的时候属性名称会用于 json 解码,但是当结构体的属性开头字母不时大写的时候我发现在结构体外不能够调用。而我一直以为是简单注释用的 `` 才是真正被用于 json 解码。另外这个符号也能被用于多行字符串赋值。
type GraphRequest struct {Prompt string `json:"prompt"`Size string `json:"size"`Watermark bool `json:"watermark"`SequentialImageGeneration string `json:"sequential_image_generation"`MaxImages int `json:"max_images"`
}func main() {// 设置请求体graphRequest := GraphRequest{Prompt: `高清动漫风格,生图级原始渲染,高细节(可做 8k 级别细节),纵向构图,画面中心为一名视觉成年人(18+)的紫色系和风战袍少女战士(和服式战斗服),人物占比高。人物描述:面容精致冷峻带战意,面部有少量新鲜血迹(湿润反光),表情为兴奋且狂傲的微笑,目光坚定、毫无迟疑,眼睛有高光和微弱发光感以强化情绪。服装与材质:和风战袍与轻甲结合(丝绸、轻纱、皮革绑带、局部金属护具),布料纤维紋理、缝线、褶皱清晰,边缘与护甲有磨损刮痕与血渍,服饰随姿态自然褶皱并微微受风扬起。姿态与武器:单膝跪地(one-knee kneel),一手握剑剑柄,剑尖垂直插入地面(沉重插地姿势),另一手自然放置或轻扶膝盖;剑为和式/直刃,剑身沾血湿润有滴落反光,剑刃刮痕细节明显,指节紧绷、肌理与动作张力真实。能量与背景:少女背后升腾交织的鲜红与紫色能量丝带(带粒子、光晕与体积光),能量在空中凝聚出隐约的巨龙轮廓/剪影(由能量流与粒子构成,非实体),象征霸道力量与内在兽性。环境氛围:战场残影(弥漫烟雾、飞焰、飘散灰烬、碎石与敌甲残片),戏剧化背光與侧逆光(rim light)突出人物轮廓,紫色冷光对比暖红主光,浅景深(焦点在人脸与剑身,背景虚化),等效镜头 50–85mm,真实光学散景(bokeh)与微小粒子动态模糊增强动感。细节强化:面部毛孔与微汗、伤口湿润反光、血液纹理、织物纤维、护甲划痕、发丝随风且带能量反光,颜色饱和但不过曝,边缘清晰,动漫写实混合风格(清晰线条+PBR 明暗渲染)。可选焦点:突出眼神与剑尖细节以增强情绪传达。
`,Size: "2048x2048",Watermark: false,SequentialImageGeneration: "disabled", // 如果需要序列化生成MaxImages: 1, // 生成最大图像数}// 将请求结构体编码成 JSONreqBody, err := json.Marshal(graphRequest)
}
在我的实际使用中我发现Windows部署Go语言的Swagger存在着意想不到的问题(如果你当前目录下的项目不是很干净,比如你在这个目录下创建了多个main.go,即使根目录下只有一个,可能也会出现一些意想不到的冲突。因此当你发现报了一些找不到包、没有包、使用不对的问题,很换一个干净的目录就能解决绝大部分问题。将来我们再来探索如何控制它的表现,毕竟我不希望在一个大项目中只有唯一的一个地方可以用main.py且只能检测根目录下的接口。我们更希望可以很好地展示各个模块各个部分的接口),而且我看其中的有些官方文档需要用Docker来部署swag服务,这无疑对Windows开发是极不友好的(WSL会比较麻烦,而且容易出更多意想不到的bug),我这里以Windows和Linux两种系统各介绍一种方法。希望给大家一份直接可运行的案例。
二、 Windows系统:
1. 新建一个项目目录用VS Code打开。然后运行初始化命令后安装所需的包
创建一个空的项目目录(这一点儿很重要,在Windows里可以避免绝大部分问题),然后按照以下的步骤依次搭建:
mkdir go-swagger-demo
cd go-swagger-demo
go mod init myproject// 在这里安装依赖
go get -u github.com/gin-gonic/gin
go get -u github.com/swaggo/files
go get -u github.com/swaggo/gin-swagger// 这里是Swagger Cli
go install github.com/swaggo/swag/cmd/swag@latest
接下来将这个保存为main.go :
package mainimport ("net/http""github.com/gin-gonic/gin"_ "myproject/docs" // swag docs (generated) - 导入以便 gin-swagger 使用swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger"
)// @title Go + Gin + Swagger Demo
// @version 1.0
// @description 示例项目:演示如何在 Go 中集成 SwaggerUI(gin + swag)
// @contact.name Example
// @contact.email example@example.com
// @host localhost:8080
// @BasePath /// Item 请求/响应示例
type Item struct {// ID 示例: 1ID int `json:"id" example:"1"`// 名称Name string `json:"name" binding:"required" example:"apple"`// 描述Description string `json:"description,omitempty" example:"a red apple"`
}// @Summary 健康检查
// @Description 返回 pong
// @Tags health
// @Produce plain
// @Success 200 {string} string "pong"
// @Router /ping [get]
func PingHandler(c *gin.Context) {c.String(http.StatusOK, "pong")
}// @Summary 创建 item
// @Description 创建并返回 item(示例)
// @Tags items
// @Accept json
// @Produce json
// @Param item body Item true "Item 对象"
// @Success 200 {object} Item
// @Failure 400 {object} map[string]interface{}
// @Router /items [post]
func CreateItemHandler(c *gin.Context) {var it Itemif err := c.ShouldBindJSON(&it); err != nil {c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})return}// 模拟设置 idit.ID = 1c.JSON(http.StatusOK, it)
}func main() {r := gin.Default()// 注册路由r.GET("/ping", PingHandler)r.POST("/items", CreateItemHandler)// Swagger UI 路由: 访问 http://localhost:8080/swagger/index.htmlurl := ginSwagger.URL("http://localhost:8080/swagger/doc.json") // 指向生成的 doc.jsonr.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler, url))// 启动if err := r.Run(":8080"); err != nil {panic(err)}
}
接下来使用Swagger初始化文档:
swag init/*
go-swagger-demo/
├── docs/ # swag 自动生成
├── go.mod
├── go.sum
├── main.go
└── README.md
*/// 然后就可以启动了go run main.go
然后http://localhost:8080/swagger/index.html就可以看到Swagger文档了:

2. 整体项目展示:多接口整合
上面的例子在实际使用中有一个非常大的弊端。真实的业务项目不可能为了配合Swagger UI进行编写,而已有的项目可能并没有考虑这个可视化。在这样的场景下如果我们仍然希望查看各个接口该怎么做呢:
首先我们简历一个test文件夹,在这个目录下有一个test.go文件。可以看到我们完全没有管SwaggerUI:
package testimport ("net/http""github.com/gin-gonic/gin"
)func PingHandler(c *gin.Context) {c.JSON(http.StatusOK, gin.H{"message": "pong",})
}func RegisterRoutes(r *gin.RouterGroup) {r.GET("/ping", PingHandler)
}
然后我们回到main.go文件中进行修改:
package mainimport ("net/http""github.com/gin-gonic/gin"_ "myproject/docs" // ✅ 必须:加载 Swagger 注解生成的文档"myproject/test" // ✅ 导入 test 模块的接口swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger"
)// @title Go Swagger Example API
// @version 1.0
// @description 这是一个使用 Gin + Swagger 的简单示例,包含 test 模块
// @host localhost:8080
// @BasePath /api/v1type Response struct {Code int `json:"code" example:"200"`Msg string `json:"msg" example:"ok"`Data interface{} `json:"data"`
}// @Summary 主接口
// @Description 返回主接口内容
// @Tags Main
// @Produce json
// @Success 200 {object} Response
// @Router /main/hello [get]
func HelloHandler(c *gin.Context) {c.JSON(http.StatusOK, Response{Code: 200,Msg: "success",Data: gin.H{"message": "Hello from main!"},})
}func main() {r := gin.Default()api := r.Group("/api/v1")// 注册 main 接口mainGroup := api.Group("/main")mainGroup.GET("/hello", HelloHandler)// ✅ 注册 test 模块接口testGroup := api.Group("/test")test.RegisterRoutes(testGroup)// Swagger 文档入口r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))r.Run(":8080")
}
运行这个命令:
swag init --parseDependency --parseInternal// 然后再启动go run main.go
我们就可以看到通过test注册的接口了:

三、Linux系统:
Linux系统的使用比Windows方便得多,由于其静态编译语言的特性,一般来说直接迁移过来是完全可行的。我们只需要重复上述的操作就可以完美地将项目部署下来,并且更加稳健。于是我使用相同的方法在远程主机上部署了一个调用远程绘图的API接口,这样我就不用每次想使用这个功能的时候都在本地运行代码了:
package mainimport ("net/http""os""path/filepath""github.com/gin-gonic/gin"swaggerFiles "github.com/swaggo/files"ginSwagger "github.com/swaggo/gin-swagger""graph/docs""graph/ppio"
)const allowedPassword = "akjnewfianwfkjlaeasasdnalkwefiz@2025" // 写死的密码,仅在代码中使用// Health godoc
// @Summary 健康检查
// @Description 返回服务健康状态
// @Tags System
// @Produce json
// @Success 200 {string} string "ok"
// @Router /health [get]
func Health(c *gin.Context) {c.String(http.StatusOK, "ok")
}type Seedream4Request struct {Password string `json:"password"` // 必填Prompt string `json:"prompt"` // 必填Size string `json:"size"` // 可选,默认 2048*2048
}// Seedream4 godoc
// @Summary 生成图像(密码验证)
// @Description 使用 Seedream 4.0 生成图像,需密码,保存到 data/ 并返回图片内容
// @Tags PPIO
// @Accept json
// @Produce image/jpeg
// @Param request body Seedream4Request true "生成参数"
// @Success 200 {file} file "JPEG 图片"
// @Failure 401 {string} string "unauthorized"
// @Failure 400 {string} string "bad request"
// @Router /ppio/seedream4 [post]
func Seedream4(c *gin.Context) {var req Seedream4Requestif err := c.ShouldBindJSON(&req); err != nil {c.String(http.StatusBadRequest, "bad request: %v", err)return}if req.Password != allowedPassword {c.String(http.StatusUnauthorized, "unauthorized")return}if req.Prompt == "" {c.String(http.StatusBadRequest, "prompt is required")return}filename, imgBytes, err := ppio.GenerateImage(req.Prompt, req.Size)if err != nil {c.String(http.StatusBadRequest, "generate image failed: %v", err)return}// 方便在 SwaggerUI 中获取文件名与下载地址c.Header("X-Image-Filename", filename)c.Header("X-Image-URL", "/data/"+filename)c.Header("Content-Type", "image/jpeg")c.Header("Content-Disposition", "inline; filename=\""+filename+"\"")c.Writer.WriteHeader(http.StatusOK)_, _ = c.Writer.Write(imgBytes)
}// DownloadImage 提供以附件形式下载已保存的图片
// @Summary 下载已生成的图片
// @Description 通过文件名返回 data/ 下的图片,作为附件下载
// @Tags PPIO
// @Produce image/jpeg
// @Param filename path string true "文件名,例如 1730870000000.jpg"
// @Success 200 {file} file "JPEG 图片"
// @Failure 404 {string} string "not found"
// @Router /ppio/image/{filename} [get]
func DownloadImage(c *gin.Context) {name := c.Param("filename")full := filepath.Join("data", name)if _, err := os.Stat(full); err != nil {c.String(http.StatusNotFound, "not found")return}c.FileAttachment(full, name)
}func main() {r := gin.Default()// 配置 Swagger 信息docs.SwaggerInfo.BasePath = "/api/v1"v1 := r.Group("/api/v1"){v1.GET("/health", Health)v1.POST("/ppio/seedream4", Seedream4)v1.GET("/ppio/image/:filename", DownloadImage)}// Swagger UI 路由r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))// 静态目录,直接访问 /data/{filename} 可在线预览或另存为r.Static("/data", "./data")// 启动服务_ = r.Run(":8888")
}
package ppioimport ("bytes""encoding/json""fmt""io""net/http""os""path/filepath""strings""time"
)// GraphRequest mirrors the Seedream 4.0 API request payload.
type GraphRequest struct {Prompt string `json:"prompt"`Size string `json:"size"`Watermark bool `json:"watermark"`SequentialImageGeneration string `json:"sequential_image_generation"`MaxImages int `json:"max_images"`
}// graphResponse is a partial response schema capturing image URLs.
type graphResponse struct {Images []string `json:"images"`
}const (baseURL = "https://api.ppinfra.com/v3/seedream-4.0"// NOTE: 为了保证开箱即用,这里复用你在 1.go 中使用的 API Key。// 如果需要更安全的配置,可以改为从环境变量读取。apiKey = "你的API密钥"
)func getBaseURL() string {if v := os.Getenv("SEEDREAM_BASE_URL"); v != "" {return v}return baseURL
}func getAPIKey() string {if v := os.Getenv("SEEDREAM_API_KEY"); v != "" {return v}return apiKey
}// GenerateImage calls the Seedream API with given prompt/size, saves to data/ and returns filename and bytes.
func GenerateImage(prompt, size string) (string, []byte, error) {normalized := normalizeSize(size)reqPayload := GraphRequest{Prompt: prompt,Size: normalized,Watermark: false,SequentialImageGeneration: "disabled",MaxImages: 1,}body, err := json.Marshal(reqPayload)if err != nil {return "", nil, fmt.Errorf("marshal request: %w", err)}client := &http.Client{Timeout: 60 * time.Second}req, err := http.NewRequest(http.MethodPost, getBaseURL(), bytes.NewBuffer(body))if err != nil {return "", nil, fmt.Errorf("create request: %w", err)}req.Header.Set("Authorization", "Bearer "+getAPIKey())req.Header.Set("Content-Type", "application/json")resp, err := client.Do(req)if err != nil {return "", nil, fmt.Errorf("do request: %w", err)}defer resp.Body.Close()respBody, err := io.ReadAll(resp.Body)if err != nil {return "", nil, fmt.Errorf("read response: %w", err)}if resp.StatusCode >= 300 {snippet := string(respBody)if len(snippet) > 512 {snippet = snippet[:512]}return "", nil, fmt.Errorf("seedream request failed: %s: %s", resp.Status, snippet)}var gr graphResponseif err := json.Unmarshal(respBody, &gr); err != nil {return "", nil, fmt.Errorf("unmarshal response: %w", err)}if len(gr.Images) == 0 {return "", nil, fmt.Errorf("no image URL returned")}// Download the first imageimgResp, err := http.Get(gr.Images[0])if err != nil {return "", nil, fmt.Errorf("download image: %w", err)}defer imgResp.Body.Close()if imgResp.StatusCode >= 300 {b, _ := io.ReadAll(imgResp.Body)snippet := string(b)if len(snippet) > 256 {snippet = snippet[:256]}return "", nil, fmt.Errorf("download image failed: %s: %s", imgResp.Status, snippet)}imgBytes, err := io.ReadAll(imgResp.Body)if err != nil {return "", nil, fmt.Errorf("read image: %w", err)}// Ensure data/ directory existsdataDir := filepath.Join(".", "data")if err := os.MkdirAll(dataDir, 0o755); err != nil {return "", nil, fmt.Errorf("ensure data dir: %w", err)}// Filename: current 13-digit timestamp (milliseconds)ts := time.Now().UnixMilli()filename := fmt.Sprintf("%d.jpg", ts)fullPath := filepath.Join(dataDir, filename)if err := os.WriteFile(fullPath, imgBytes, 0o644); err != nil {return "", nil, fmt.Errorf("write file: %w", err)}return filename, imgBytes, nil
}// normalizeSize converts "2048*2048" to "2048x2048" to match API expectations.
func normalizeSize(size string) string {s := strings.TrimSpace(size)if s == "" {s = "2048*2048"}s = strings.ReplaceAll(s, "*", "x")return s
}
最后我们就可以看到:

成功返回了AI绘制的图片!
