news 2026/11/5 8:19:52

用 beego 框架實現部落格增刪改:MVC 部落格系統完整實戰(build-web-application-with-golang 第 13.5 節)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 beego 框架實現部落格增刪改:MVC 部落格系統完整實戰(build-web-application-with-golang 第 13.5 節)
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载

本篇技術指南以開源電子書《build-web-application-with-golang》第 13.5 節為核心,講解如何基於前幾節自研的 beego 框架快速搭建一個具備瀏覽、新增、修改、刪除完整能力的部落格系統。文中將完整繼承原章節的目錄結構、路由規則、資料庫設計、五個控制器、beedb 資料層與五個範本檔案,並結合本倉庫第 13 章其他小節的框架源碼(路由分發、控制器生命週期、範本渲染)做縱深拆解。讀完本章你可以掌握 REST 風格 MVC 應用的完整搭建流程,並理解 beego 框架從請求進來到頁面輸出的每一環。

本節在整個框架章節中的位置

第 13 章在 13.0.md 中規劃了「如何設計一個 Web 框架」的完整路線:先講 MVC 專案規劃與資料流(13.1 專案規劃),再實現支援正則參數與 REST 方法分發的路由器(13.2 自訂路由器設計),接著設計公共控制器基底類別與自動渲染機制(13.3 controller 設計),並補上日誌分級與配置解析(13.4 日誌和配置設計)。

本節 13.5 實現部落格的增刪改 是整個章節的「驗收工程」:把前面設計的框架能力全部串起來,落地成一個可執行的部落格站點。整個專案只有三個目錄——controllers、models、views,外加一個入口檔案main.go,卻完整覆蓋了 CRUD 的全部場景,正好呼應 13.3 中「controller 是 MVC 中必不可少的一環,而 Model 與 View 在部分場景可以省略」的設計哲學。

部落格專案目錄結構

原章節給出的部落格目錄如下:

. ├── controllers │ ├── delete.go │ ├── edit.go │ ├── index.go │ ├── new.go │ └── view.go ├── main.go ├── models │ └── model.go └── views ├── edit.tpl ├── index.tpl ├── layout.tpl ├── new.tpl └── view.tpl

這套結構與 13.1 專案規劃 中設計的目錄藍圖一一對應:

  • main.go:應用入口,負責初始化框架、註冊路由、啟動監聽;
  • controllers/:控制器入口,五個檔案分別對應首頁、詳細、新增、編輯、刪除五種業務;
  • models/model.go:資料庫處理模組,封裝全部資料存取;
  • views/:檢視範本目錄,其中layout.tpl是全域性佈局,其餘四個是具體頁面。

可以看到 13.1 規劃中的conf、utils、static等目錄在這裡被有意省略了——部落格範例足夠簡單,不需要獨立配置模組與靜態檔案目錄,這正是「框架只是工具,適合自己的才是最好的」思想的體現。

部落格路由:五條規則對應五大控制器

部落格的主要路由規則如下(位於入口檔案main.go中,依據目錄結構與本節路由清單可以推斷):

//顯示部落格首頁 beego.Router("/", &controllers.IndexController{}) //檢視部落格詳細資訊 beego.Router("/view/:id([0-9]+)", &controllers.ViewController{}) //建立部落格博文 beego.Router("/new", &controllers.NewController{}) //刪除博文 beego.Router("/delete/:id([0-9]+)", &controllers.DeleteController{}) //編輯博文 beego.Router("/edit/:id([0-9]+)", &controllers.EditController{})

這五條規則涵蓋了部落格的全部 URL 空間,背後正是 13.2 自訂路由器設計 中實現的兩大能力:

  1. 正則參數支援:/view/:id([0-9]+)這種寫法解決了 Go 內建http.DefaultServeMux不支援/user/:uid泛型匹配的限制。路由註冊時會把:id替換成正則([0-9]+)並編譯,請求到來時透過regexp.FindStringSubmatch取出子匹配作為參數,存入Context.Params,控制器內即可透過this.Ctx.Input.Params()[":id"]讀取。

  2. REST 方法分發:路由把請求轉發到 struct 而非函式,13.2 的 ServeHTTP 實現 會依r.Method動態呼叫Init → Prepare → Get/Post/Put/Delete… → Render → Finish。因此/new這條路由同時服務 GET(顯示表單)與 POST(提交表單)兩種請求,分別命中NewController的Get()與Post()方法。

值得留意的是/delete/:id([0-9]+)與/edit/:id([0-9]+)都使用了[0-9]+正則約束,這保證了 URL 中的 id 必然是純數字,從入口處就過濾掉了非法請求,也是 13.4 中提到的「安全檢測」思想在路由層的體現。

資料庫結構:最小化部落格資料表

資料庫設計最簡單的部落格資訊:

CREATE TABLE entries ( id INT AUTO_INCREMENT, title TEXT, content TEXT, created DATETIME, primary key (id) );

這張entries表只有四個欄位:

  • id:自增主鍵,INT AUTO_INCREMENT,由資料庫自動分配,與 model 層Blog.Id的PK標記對應;
  • title:標題,TEXT型別;
  • content:正文,TEXT型別;
  • created:建立/更新時間,DATETIME,由控制器在寫入時以time.Now()賦值。

後續 model 層程式碼中資料庫連線字串是blog/astaxie/123456(格式為資料庫名/使用者名稱/密碼),因此需要預先建立名為blog的資料庫並授權astaxie使用者。本倉庫第 5 章提供了多個可對照的建表範例,例如 ch.5.2 的 schema.sql、ch.5.4 的 schema.sql、ch.5.5 的 schema.sql,均展示了 Go 應用配合 MySQL 建表與連線的標準姿勢。

控制器層:五個控制器實現 REST 增刪改查

IndexController:首頁列表

type IndexController struct { beego.Controller } func (this *IndexController) Get() { this.Data["blogs"] = models.GetAll() this.Layout = "layout.tpl" this.TplName = "index.tpl" }

IndexController只實現了Get():呼叫models.GetAll()取回全部博文存入this.Data["blogs"],然後設定佈局範本layout.tpl與內容範本index.tpl。根據 13.3 controller 設計,Get()執行完畢後若全域性AutoRender=true,框架會自動呼叫Render(),把layout.tpl作為外殼、index.tpl的渲染結果嵌入{{.LayoutContent}}後輸出。

ViewController:檢視單篇詳細資訊

type ViewController struct { beego.Controller } func (this *ViewController) Get() { id, _ := strconv.Atoi(this.Ctx.Input.Params()[":id"]) this.Data["Post"] = models.GetBlog(id) this.Layout = "layout.tpl" this.TplName = "view.tpl" }

ViewController從路由參數中取出:id:this.Ctx.Input.Params()[":id"]對應 13.2 路由實現中存入Context.Params的正則子匹配。由於路由正則限定[0-9]+,此處strconv.Atoi的錯誤可以被安全忽略(id, _ :=)。取到單篇博文後存入this.Data["Post"],交由view.tpl渲染。

NewController:新增博文

type NewController struct { beego.Controller } func (this *NewController) Get() { this.Layout = "layout.tpl" this.TplName = "new.tpl" } func (this *NewController) Post() { inputs := this.Input() var blog models.Blog blog.Title = inputs.Get("title") blog.Content = inputs.Get("content") blog.Created = time.Now() models.SaveBlog(blog) this.Ctx.Redirect(302, "/") }

這是 REST 控制器「一對二」的典型範例:同一條/new路由,GET 請求渲染new.tpl表單頁,POST 請求處理表單提交。Post()的流程是:

  1. this.Input()取得請求輸入物件,inputs.Get("title")與inputs.Get("content")讀取表單欄位;
  2. 組裝models.Blog,其中Created由time.Now()填充,Id留空——這對 beedb 的Save操作至關重要(詳見 model 層一節);
  3. 呼叫models.SaveBlog(blog)寫入資料庫;
  4. this.Ctx.Redirect(302, "/")以 302 跳轉回首頁,符合 13.3 提到的「沒有頁面輸出的 302 調整」場景——這種場景完全可以不寫 View。

EditController:編輯博文

type EditController struct { beego.Controller } func (this *EditController) Get() { id, _ := strconv.Atoi(this.Ctx.Input.Params()[":id"]) this.Data["Post"] = models.GetBlog(id) this.Layout = "layout.tpl" this.TplName = "edit.tpl" } func (this *EditController) Post() { inputs := this.Input() var blog models.Blog blog.Id, _ = strconv.Atoi(inputs.Get("id")) blog.Title = inputs.Get("title") blog.Content = inputs.Get("content") blog.Created = time.Now() models.SaveBlog(blog) this.Ctx.Redirect(302, "/") }

EditController與NewController結構高度對稱,差別在於兩點:

  • Get()需要先依:id載入既有博文,存入this.Data["Post"],讓edit.tpl用{{.Post.Title}}等預填表單值;
  • Post()必須從表單隱藏欄位id讀取主鍵(對應edit.tpl中的<input type="hidden" name="id" value="{{.Post.Id}}">),這樣Save時 beedb 才能判斷是「更新既有記錄」而非「新增記錄」。

DeleteController:刪除博文

type DeleteController struct { beego.Controller } func (this *DeleteController) Get() { id, _ := strconv.Atoi(this.Ctx.Input.Params()[":id"]) blog := models.GetBlog(id) this.Data["Post"] = blog models.DelBlog(blog) this.Ctx.Redirect(302, "/") }

DeleteController是「副作用型 GET」的簡化寫法:先依 id 查出Blog物件,再呼叫models.DelBlog(blog)刪除,最後 302 回首頁。刪除動作直接掛在 GET 請求上(頁面上的Delete連結即為普通<a href>),這在真實專案中通常會換成 DELETE 方法或至少加上二次確認,但作為框架教學範例,它用最少的程式碼演示了 beedb 的刪除介面。

Model 層:透過 beedb 封裝資料存取

package models import ( "database/sql" "github.com/astaxie/beedb" _ "github.com/ziutek/mymysql/godrv" "time" ) type Blog struct { Id int `PK` Title string Content string Created time.Time } func GetLink() beedb.Model { db, err := sql.Open("mymysql", "blog/astaxie/123456") if err != nil { panic(err) } orm := beedb.New(db) return orm } func GetAll() (blogs []Blog) { db := GetLink() db.FindAll(&blogs) return } func GetBlog(id int) (blog Blog) { db := GetLink() db.Where("id=?", id).Find(&blog) return } func SaveBlog(blog Blog) (bg Blog) { db := GetLink() db.Save(&blog) return bg } func DelBlog(blog Blog) { db := GetLink() db.Delete(&blog) return }

Model 層的全部邏輯就是「一個結構體 + 五個函式」:

  • Blog結構體:Id int帶有PK標記,beedb 依此識別主鍵欄位並對映到資料表的id列;Created time.Time對映到DATETIME型別的created列。
  • GetLink():每次操作都建立新的連線——sql.Open("mymysql", "blog/astaxie/123456")開啟 MySQL 連線(DSN 格式為資料庫/使用者/密碼),再透過beedb.New(db)包裝成 beedb 的 ORM 模型。需要注意的是,連線資訊目前是硬編碼的;13.4 日誌和配置設計 介紹的beego.AppConfig.String("url")讀取 ini 配置的機制,正是把這類「伺服器相關配置」抽離到app.conf的標準做法。
  • GetAll():db.FindAll(&blogs)一次查出全部記錄並填充[]Blog切片,供首頁列表使用。
  • GetBlog(id):db.Where("id=?", id).Find(&blog)以參數化查詢定位單篇博文,?佔位符避免 SQL 注入。
  • SaveBlog(blog):db.Save(&blog)是 beedb 的「有主鍵則更新、無主鍵則插入」操作,因此NewController.Post()中Id為零值時會新增,EditController.Post()中Id來自隱藏欄位時會更新。另外,從程式碼可以看出SaveBlog宣告了回傳值bg但始終未賦值,實際回傳的是零值Blog,而兩個呼叫端目前都沒有使用其回傳值,因此不影響功能——這是一處值得讀者注意的小瑕疵。
  • DelBlog(blog):db.Delete(&blog)依主鍵刪除記錄。

依賴方面,model 層引入了github.com/astaxie/beedb(輕量 ORM)與github.com/ziutek/mymysql/godrv(MySQL 驅動,透過空白匯入註冊mymysql驅動名),執行前需要先go get這兩個依賴。

View 層:五個範本完成頁面渲染

layout.tpl:全域性佈局

<html> <head> <title>My Blog</title> <style> #menu { width: 200px; float: right; } </style> </head> <body> <ul id="menu"> <li><a href="/">Home</a></li> <li><a href="/new">New Post</a></li> </ul> {{.LayoutContent}} </body> </html>

layout.tpl是站點外殼:右側固定導覽列(Home 與 New Post),正文位置由{{.LayoutContent}}佔位。這個佔位符正是 13.3 controller 設計 中Render()的協作成果——當控制器設定了this.Layout,框架會用template.ParseFiles同時解析佈局與內容範本,再以ExecuteTemplate(w, c.TplNames, c.Data)把子範本渲染結果填入{{.LayoutContent}}。

index.tpl:首頁列表

<h1>Blog posts</h1> <ul> {{range .blogs}} <li> <a href="/view/{{.Id}}">{{.Title}}</a> from {{.Created}} <a href="/edit/{{.Id}}">Edit</a> <a href="/delete/{{.Id}}">Delete</a> </li> {{end}} </ul>

index.tpl使用 Go 範本引擎的{{range .blogs}}遍歷控制器注入的博文切片,每篇博文輸出標題連結(跳轉/view/{{.Id}})、建立時間,以及 Edit、Delete 兩個操作連結(分別指向/edit/{{.Id}}與/delete/{{.Id}})。這三個連結與前文五條路由完全對應,構成了完整的操作入口。

view.tpl:單篇詳細

<h1>{{.Post.Title}}</h1> {{.Post.Created}}<br/> {{.Post.Content}}

view.tpl直接以點路徑取用ViewController注入的Post物件欄位,展示標題、時間與正文。

new.tpl:新增表單

<h1>New Blog Post</h1> <form action="" method="post"> 標題:<input type="text" name="title"><br> 內容:<textarea name="content" colspan="3" rowspan="10"></textarea> <input type="submit"> </form>

new.tpl是新增頁表單:action=""表示提交到當前 URL(即/new),method="post"觸發NewController.Post();表單欄位名title、content與控制器中inputs.Get("title")、inputs.Get("content")一一對應。

edit.tpl:編輯表單

<h1>Edit {{.Post.Title}}</h1> <h1>New Blog Post</h1> <form action="" method="post"> 標題:<input type="text" name="title" value="{{.Post.Title}}"><br> 內容:<textarea name="content" colspan="3" rowspan="10">{{.Post.Content}}</textarea> <input type="hidden" name="id" value="{{.Post.Id}}"> <input type="submit"> </form>

edit.tpl與new.tpl的關鍵差異在於三處預填值:標題輸入框的value="{{.Post.Title}}"、正文 textarea 內的{{.Post.Content}},以及最關鍵的<input type="hidden" name="id" value="{{.Post.Id}}">隱藏主鍵欄位——它確保EditController.Post()能從inputs.Get("id")恢復記錄主鍵,進而讓 beedb 執行更新而非插入。另外從範本程式碼可以看到頁面標題出現了Edit {{.Post.Title}}與New Blog Post兩行<h1>,後者應是範本複用時的殘留,不影響功能,但屬於可以清理的小瑕疵。

請求生命週期:一次完整的新增與編輯流程

把控制器、路由與資料層串起來,一次「新增博文」請求的完整路徑是:

  1. 瀏覽器 GET/new→ 路由器比對/new規則 → 以反射建立NewController實例,執行Init → Prepare → Get()(見 13.2 的ServeHTTP分發邏輯);
  2. Get()設定Layout = "layout.tpl"與TplName = "new.tpl",Render()將表單頁嵌入佈局後輸出;
  3. 使用者填表並 POST 到/new→ 路由器依 method 改為執行Post();
  4. Post()讀取表單、組裝Blog、呼叫models.SaveBlog寫庫,再Redirect(302, "/");
  5. 瀏覽器跟隨跳轉 GET/→IndexController.Get()呼叫GetAll()查出最新列表並渲染,新博文出現在首頁。

「編輯」流程同理,只是 GET 階段多了「依 id 查詢並預填」,POST 階段多了「讀取隱藏 id 走更新語義」。這與 13.1 專案規劃 中描述的框架資料流(入口初始化 → 路由分發 → 控制器裝載 model → 輸出 view)完全吻合,本節部落格就是該流程圖的一次具體落地。

執行前置條件與注意事項

在執行這個部落格範例前,需要準備:

  1. GOPATH 與專案目錄:依 13.1 專案規劃,設定GOPATH並保證其下有src/pkg/bin三個目錄,部落格原始碼放在$GOPATH/src下;
  2. 依賴庫:go get github.com/astaxie/beedb與go get github.com/ziutek/mymysql,前者是 ORM,後者提供mymysql驅動;
  3. 資料庫:依blog/astaxie/123456建立名為blog的資料庫、使用者astaxie、密碼123456,並執行本節的CREATE TABLE entries建表語句;
  4. 範本命名:控制器中Layout、TplName均為相對於views目錄的檔名;13.3 提到框架預設範本擴展名TplExt = "tpl",本節的.tpl檔案與之匹配;
  5. 自動渲染:確認AutoRender為開啟狀態,Get()/Post()執行完畢後框架才會自動呼叫Render()輸出頁面(13.2、13.3 中的ServeHTTP與Render()實現)。

延伸閱讀

  • 框架總體規劃與資料流:13.0 如何設計一個 Web 框架、13.1 專案規劃
  • 路由的正則參數與 REST 分發源碼:13.2 自訂路由器設計
  • 控制器基底類別與渲染機制:13.3 controller 設計
  • 日誌分級與 ini 配置解析:13.4 日誌和配置設計
  • 本章總結與後續路線:13.6 小結
  • 全書目錄:preface.md
  • 相關範例程式碼:第 5 章資料庫範例、範本範例 login.gtpl
  • 文档
  • 教程

【免费下载链接】build-web-application-with-golang

A golang ebook intro how to build a web with golang

项目地址:https://gitcode.com/gh_mirrors/bu/build-web-application-with-golang
点击查看免费下载
上一篇:告别等待:LanceDB异步API让你的AI应用并发能力飙升10倍
下一篇:Kaniko构建镜像的元数据暴露:Prometheus指标深度实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 8:19:47

NB-IoT接入OneNET实战:智能井盖项目从配置到部署完整指南

去年接了一个智能井盖监测的项目&#xff0c;现场条件直接把WiFi方案判了死刑。地下井室里没有网线、没有供电、更没有WiFi信号&#xff0c;金属井盖盖上之后普通无线基本就失联了。最后定下来的方案就是NB-IoT加移动OneNET平台&#xff0c;这也是我现在做这类电池供电的分散式…

作者头像 李华
网站建设 2026/11/5 8:19:46

Java ERP源码二次开发实战:从环境搭建到权限事务避坑指南

简介&#xff1a;这是一套基于Java开发的企业级ERP系统完整源码&#xff0c;面向有一定Java Web基础、希望深入理解企业级管理系统架构的开发者与学习者&#xff0c;可用于二次开发、课程设计或技术选型参考。压缩包共收录2000个文件&#xff0c;整体约53.27MB&#xff0c;其中…

作者头像 李华
网站建设 2026/11/5 8:19:47

pdf图片转word用什么软件?新手零踩坑实用工具合集

日常办公、学生写论文、整理资料时&#xff0c;很多人都会遇到一个难题&#xff1a;拿到的PDF是扫描图片格式&#xff0c;复制不了文字&#xff0c;无法编辑修改。普通的PDF转换工具完全没用&#xff0c;想要把图片PDF转换成可编辑的Word文档&#xff0c;核心关键就是自带OCR文…

作者头像 李华
网站建设 2026/11/1 12:28:47

整数划分的三种解法:递归、动态规划与回溯实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华