3.3.4 Gin 渲染与模板 #
Gin 框架提供了多种渲染方式和模板引擎支持,能够满足不同类型应用的需求。本节将深入探讨 Gin 的渲染机制、HTML 模板引擎、静态文件服务以及响应格式化技术。
多种渲染方式 #
JSON 渲染 #
JSON 是现代 Web API 最常用的数据交换格式:
package main
import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)
// 数据结构定义
type User struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
Profile *Profile `json:"profile,omitempty"`
}
type Profile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
}
func setupJSONRendering() *gin.Engine {
r := gin.Default()
// 1. 基础 JSON 响应
r.GET("/users/:id", func(c *gin.Context) {
user := User{
ID: 1,
Username: "john_doe",
Email: "[email protected]",
CreatedAt: time.Now(),
}
c.JSON(http.StatusOK, user)
})
// 2. 使用 gin.H 快速构建 JSON
r.GET("/status", func(c *gin.Context) {
c.JSON(200, gin.H{
"status": "ok",
"timestamp": time.Now().Unix(),
"version": "1.0.0",
})
})
// 3. 条件性 JSON 字段
r.GET("/users/:id/profile", func(c *gin.Context) {
includePrivate := c.Query("include_private") == "true"
user := User{
ID: 1,
Username: "john_doe",
Email: "[email protected]",
}
if includePrivate {
user.Profile = &Profile{
FirstName: "John",
LastName: "Doe",
Avatar: "/avatars/john.jpg",
Bio: "Software Developer",
}
}
c.JSON(200, user)
})
// 4. 自定义 JSON 编码
r.GET("/custom-json", func(c *gin.Context) {
data := map[string]interface{}{
"message": "Hello, 世界",
"unicode": "支持中文",
"html": "<script>alert('xss')</script>",
}
// 禁用 HTML 转义
c.PureJSON(200, data)
})
// 5. 安全的 JSON 响应(防止 JSON 劫持)
r.GET("/secure-json", func(c *gin.Context) {
data := []string{"item1", "item2", "item3"}
// 添加 ")]}',\n" 前缀防止 JSON 劫持
c.SecureJSON(200, data)
})
// 6. JSONP 支持
r.GET("/jsonp", func(c *gin.Context) {
data := gin.H{
"message": "JSONP response",
"data": []int{1, 2, 3, 4, 5},
}
// 支持 JSONP 回调
c.JSONP(200, data)
})
return r
}
XML 和其他格式渲染 #
import (
"encoding/xml"
)
// XML 数据结构
type XMLUser struct {
XMLName xml.Name `xml:"user"`
ID uint `xml:"id,attr"`
Username string `xml:"username"`
Email string `xml:"email"`
CreatedAt string `xml:"created_at"`
}
func setupMultiFormatRendering() *gin.Engine {
r := gin.Default()
// 1. XML 渲染
r.GET("/users/:id.xml", func(c *gin.Context) {
user := XMLUser{
ID: 1,
Username: "john_doe",
Email: "[email protected]",
CreatedAt: time.Now().Format(time.RFC3339),
}
c.XML(200, user)
})
// 2. YAML 渲染
r.GET("/config.yaml", func(c *gin.Context) {
config := gin.H{
"database": gin.H{
"host": "localhost",
"port": 5432,
"username": "admin",
"password": "secret",
},
"redis": gin.H{
"host": "localhost",
"port": 6379,
},
}
c.YAML(200, config)
})
// 3. Protocol Buffers 渲染
r.GET("/users/:id.proto", func(c *gin.Context) {
// 假设有 protobuf 定义的结构
data := []int{1, 2, 3, 4, 5}
c.ProtoBuf(200, &data)
})
// 4. 纯文本渲染
r.GET("/plain", func(c *gin.Context) {
c.String(200, "Hello, %s! Current time: %s", "World", time.Now().Format(time.RFC3339))
})
// 5. 重定向
r.GET("/redirect", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://example.com")
})
// 6. 文件下载
r.GET("/download/:filename", func(c *gin.Context) {
filename := c.Param("filename")
c.FileAttachment("./uploads/"+filename, filename)
})
// 7. 数据流响应
r.GET("/stream", func(c *gin.Context) {
c.Stream(func(w io.Writer) bool {
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: Message %d\n\n", i)
time.Sleep(time.Second)
}
return false
})
})
return r
}
内容协商 #
根据客户端请求的 Accept 头自动选择响应格式:
func setupContentNegotiation() *gin.Engine {
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
user := User{
ID: 1,
Username: "john_doe",
Email: "[email protected]",
CreatedAt: time.Now(),
}
// 根据 Accept 头选择响应格式
accept := c.GetHeader("Accept")
switch {
case strings.Contains(accept, "application/xml"):
xmlUser := XMLUser{
ID: user.ID,
Username: user.Username,
Email: user.Email,
CreatedAt: user.CreatedAt.Format(time.RFC3339),
}
c.XML(200, xmlUser)
case strings.Contains(accept, "application/yaml"):
c.YAML(200, user)
case strings.Contains(accept, "text/plain"):
c.String(200, "User: %s (%s)", user.Username, user.Email)
default:
c.JSON(200, user)
}
})
return r
}
HTML 模板引擎 #
基础模板使用 #
func setupHTMLTemplates() *gin.Engine {
r := gin.Default()
// 1. 加载模板文件
r.LoadHTMLGlob("templates/*")
// 2. 或者加载特定模板
// r.LoadHTMLFiles("templates/index.html", "templates/user.html")
// 3. 渲染模板
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "Welcome",
"message": "Hello, Gin Templates!",
"users": []User{
{ID: 1, Username: "alice", Email: "[email protected]"},
{ID: 2, Username: "bob", Email: "[email protected]"},
},
})
})
return r
}
模板文件示例 #
创建模板文件 templates/index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{.title}}</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
}
.user {
border: 1px solid #ddd;
padding: 10px;
margin: 10px 0;
}
.header {
color: #333;
}
</style>
</head>
<body>
<h1 class="header">{{.title}}</h1>
<p>{{.message}}</p>
{{if .users}}
<h2>Users</h2>
{{range .users}}
<div class="user">
<h3>{{.Username}}</h3>
<p>Email: {{.Email}}</p>
<p>ID: {{.ID}}</p>
</div>
{{end}} {{else}}
<p>No users found.</p>
{{end}}
<!-- 包含其他模板 -->
{{template "footer.html" .}}
</body>
</html>
创建 templates/footer.html
:
<footer
style="margin-top: 40px; padding-top: 20px; border-top: 1px solid #eee;"
>
<p>© 2024 Gin Application. All rights reserved.</p>
<p>Generated at: {{.timestamp}}</p>
</footer>
高级模板功能 #
import (
"html/template"
"strings"
"time"
)
func setupAdvancedTemplates() *gin.Engine {
r := gin.Default()
// 1. 自定义模板函数
r.SetFuncMap(template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
},
"add": func(a, b int) int {
return a + b
},
"multiply": func(a, b int) int {
return a * b
},
"truncate": func(s string, length int) string {
if len(s) <= length {
return s
}
return s[:length] + "..."
},
"safe": func(s string) template.HTML {
return template.HTML(s)
},
})
// 2. 加载嵌套模板
r.LoadHTMLGlob("templates/**/*")
// 3. 使用自定义函数的模板
r.GET("/advanced", func(c *gin.Context) {
c.HTML(200, "advanced.html", gin.H{
"title": "Advanced Template",
"username": "john_doe",
"content": "<p>This is <strong>HTML</strong> content</p>",
"createdAt": time.Now(),
"count": 42,
"longText": "This is a very long text that needs to be truncated for display purposes.",
})
})
return r
}
创建 templates/advanced.html
:
<!DOCTYPE html>
<html>
<head>
<title>{{.title}}</title>
</head>
<body>
<h1>{{upper .title}}</h1>
<!-- 使用自定义函数 -->
<p>Username: {{upper .username}}</p>
<p>Created: {{formatDate .createdAt}}</p>
<p>Count + 10 = {{add .count 10}}</p>
<p>Count * 2 = {{multiply .count 2}}</p>
<!-- 截断长文本 -->
<p>{{truncate .longText 50}}</p>
<!-- 安全渲染 HTML -->
<div>{{safe .content}}</div>
<!-- 条件渲染 -->
{{if gt .count 40}}
<p>Count is greater than 40</p>
{{else}}
<p>Count is 40 or less</p>
{{end}}
</body>
</html>
模板继承 #
创建基础模板 templates/layouts/base.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{block "title" .}}Default Title{{end}}</title>
<link rel="stylesheet" href="/static/css/style.css" />
{{block "head" .}}{{end}}
</head>
<body>
<header>
<nav>
<a href="/">Home</a>
<a href="/users">Users</a>
<a href="/about">About</a>
</nav>
</header>
<main>
{{block "content" .}}
<p>Default content</p>
{{end}}
</main>
<footer>
<p>© 2024 My Application</p>
</footer>
{{block "scripts" .}}{{end}}
</body>
</html>
创建继承模板 templates/users/list.html
:
{{template "layouts/base.html" .}} {{define "title"}}User List{{end}} {{define
"head"}}
<style>
.user-card {
border: 1px solid #ddd;
padding: 15px;
margin: 10px 0;
}
</style>
{{end}} {{define "content"}}
<h1>Users</h1>
{{range .users}}
<div class="user-card">
<h3>{{.Username}}</h3>
<p>{{.Email}}</p>
<a href="/users/{{.ID}}">View Details</a>
</div>
{{end}} {{end}} {{define "scripts"}}
<script>
console.log("User list page loaded");
</script>
{{end}}
静态文件服务 #
基础静态文件服务 #
func setupStaticFiles() *gin.Engine {
r := gin.Default()
// 1. 基础静态文件服务
r.Static("/static", "./static")
// 2. 静态文件服务(带路径前缀)
r.StaticFS("/assets", http.Dir("./assets"))
// 3. 单个文件服务
r.StaticFile("/favicon.ico", "./static/favicon.ico")
// 4. 嵌入式静态文件(Go 1.16+)
// r.StaticFS("/embedded", http.FS(embeddedFiles))
return r
}
高级静态文件配置 #
import (
"net/http"
"path/filepath"
"strings"
)
func setupAdvancedStaticFiles() *gin.Engine {
r := gin.Default()
// 1. 自定义静态文件中间件
r.Use(staticFileMiddleware("./static", "/static"))
// 2. 带缓存的静态文件服务
r.Use(staticWithCache("./public", "/public", 24*time.Hour))
// 3. 压缩静态文件服务
r.Use(compressedStatic("./assets", "/assets"))
return r
}
// 自定义静态文件中间件
func staticFileMiddleware(root, prefix string) gin.HandlerFunc {
fileServer := http.FileServer(http.Dir(root))
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, prefix) {
// 移除前缀
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, prefix)
// 安全检查,防止目录遍历攻击
if strings.Contains(c.Request.URL.Path, "..") {
c.AbortWithStatus(404)
return
}
fileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
}
}
// 带缓存的静态文件服务
func staticWithCache(root, prefix string, maxAge time.Duration) gin.HandlerFunc {
fileServer := http.FileServer(http.Dir(root))
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, prefix) {
// 设置缓存头
c.Header("Cache-Control", fmt.Sprintf("public, max-age=%d", int(maxAge.Seconds())))
c.Header("Expires", time.Now().Add(maxAge).Format(http.TimeFormat))
// 检查文件是否存在
filePath := filepath.Join(root, strings.TrimPrefix(c.Request.URL.Path, prefix))
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.AbortWithStatus(404)
return
}
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, prefix)
fileServer.ServeHTTP(c.Writer, c.Request)
c.Abort()
}
}
}
// 压缩静态文件服务
func compressedStatic(root, prefix string) gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, prefix) {
// 检查客户端是否支持 gzip
if strings.Contains(c.GetHeader("Accept-Encoding"), "gzip") {
gzipPath := filepath.Join(root, strings.TrimPrefix(c.Request.URL.Path, prefix)+".gz")
if _, err := os.Stat(gzipPath); err == nil {
c.Header("Content-Encoding", "gzip")
c.Header("Content-Type", getContentType(c.Request.URL.Path))
c.File(gzipPath)
c.Abort()
return
}
}
// 提供原始文件
originalPath := filepath.Join(root, strings.TrimPrefix(c.Request.URL.Path, prefix))
c.File(originalPath)
c.Abort()
}
}
}
func getContentType(path string) string {
ext := filepath.Ext(path)
switch ext {
case ".css":
return "text/css"
case ".js":
return "application/javascript"
case ".html":
return "text/html"
case ".json":
return "application/json"
case ".png":
return "image/png"
case ".jpg", ".jpeg":
return "image/jpeg"
case ".gif":
return "image/gif"
case ".svg":
return "image/svg+xml"
default:
return "application/octet-stream"
}
}
响应格式化 #
统一响应格式 #
// 响应包装器
type ResponseWrapper struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Message string `json:"message,omitempty"`
Error *ErrorInfo `json:"error,omitempty"`
Meta *MetaInfo `json:"meta,omitempty"`
Timestamp int64 `json:"timestamp"`
}
type ErrorInfo struct {
Code string `json:"code"`
Message string `json:"message"`
Details interface{} `json:"details,omitempty"`
}
type MetaInfo struct {
RequestID string `json:"request_id,omitempty"`
Version string `json:"version,omitempty"`
Pagination *Pagination `json:"pagination,omitempty"`
}
type Pagination struct {
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
TotalPages int `json:"total_pages"`
}
// 响应辅助函数
func SuccessResponse(c *gin.Context, data interface{}, message ...string) {
response := ResponseWrapper{
Success: true,
Data: data,
Timestamp: time.Now().Unix(),
}
if len(message) > 0 {
response.Message = message[0]
}
if requestID, exists := c.Get("request_id"); exists {
response.Meta = &MetaInfo{
RequestID: requestID.(string),
Version: "v1",
}
}
c.JSON(200, response)
}
func SuccessWithPagination(c *gin.Context, data interface{}, pagination Pagination) {
response := ResponseWrapper{
Success: true,
Data: data,
Timestamp: time.Now().Unix(),
Meta: &MetaInfo{
Version: "v1",
Pagination: &pagination,
},
}
if requestID, exists := c.Get("request_id"); exists {
response.Meta.RequestID = requestID.(string)
}
c.JSON(200, response)
}
func ErrorResponse(c *gin.Context, statusCode int, code, message string, details interface{}) {
response := ResponseWrapper{
Success: false,
Error: &ErrorInfo{
Code: code,
Message: message,
Details: details,
},
Timestamp: time.Now().Unix(),
}
if requestID, exists := c.Get("request_id"); exists {
response.Meta = &MetaInfo{
RequestID: requestID.(string),
Version: "v1",
}
}
c.JSON(statusCode, response)
}
完整的渲染示例应用 #
func main() {
r := gin.Default()
// 设置模板函数
r.SetFuncMap(template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
},
"upper": strings.ToUpper,
})
// 加载模板
r.LoadHTMLGlob("templates/**/*")
// 静态文件服务
r.Static("/static", "./static")
r.StaticFile("/favicon.ico", "./static/favicon.ico")
// API 路由
api := r.Group("/api/v1")
{
api.GET("/users", func(c *gin.Context) {
users := []User{
{ID: 1, Username: "alice", Email: "[email protected]", CreatedAt: time.Now()},
{ID: 2, Username: "bob", Email: "[email protected]", CreatedAt: time.Now()},
}
SuccessResponse(c, users, "Users retrieved successfully")
})
api.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
if id == "1" {
user := User{
ID: 1,
Username: "alice",
Email: "[email protected]",
CreatedAt: time.Now(),
}
SuccessResponse(c, user)
} else {
ErrorResponse(c, 404, "USER_NOT_FOUND", "User not found", nil)
}
})
}
// Web 页面路由
r.GET("/", func(c *gin.Context) {
c.HTML(200, "index.html", gin.H{
"title": "Home Page",
"message": "Welcome to Gin Application",
"timestamp": time.Now(),
})
})
r.GET("/users", func(c *gin.Context) {
users := []User{
{ID: 1, Username: "alice", Email: "[email protected]", CreatedAt: time.Now()},
{ID: 2, Username: "bob", Email: "[email protected]", CreatedAt: time.Now()},
}
c.HTML(200, "users/list.html", gin.H{
"title": "User List",
"users": users,
})
})
r.Run(":8080")
}
通过本节的学习,你已经全面掌握了 Gin 框架的渲染与模板功能。这些技术能够帮助你构建既能提供 API 服务又能渲染 Web 页面的全栈应用。合理使用这些渲染方式可以大大提高开发效率和用户体验。
至此,我们已经完成了 Gin 框架核心功能的全面学习,包括路由与参数绑定、中间件机制、错误处理与验证以及渲染与模板。这些知识为你使用 Gin 开发高质量的 Web 应用奠定了坚实的基础。