- 文档
- 教程
【免费下载链接】build-web-application-with-golang
A golang ebook intro how to build a web 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 自訂路由器設計 中實現的兩大能力:
正則參數支援:
/view/:id([0-9]+)這種寫法解決了 Go 內建http.DefaultServeMux不支援/user/:uid泛型匹配的限制。路由註冊時會把:id替換成正則([0-9]+)並編譯,請求到來時透過regexp.FindStringSubmatch取出子匹配作為參數,存入Context.Params,控制器內即可透過this.Ctx.Input.Params()[":id"]讀取。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()的流程是:
this.Input()取得請求輸入物件,inputs.Get("title")與inputs.Get("content")讀取表單欄位;- 組裝
models.Blog,其中Created由time.Now()填充,Id留空——這對 beedb 的Save操作至關重要(詳見 model 層一節); - 呼叫
models.SaveBlog(blog)寫入資料庫; 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>,後者應是範本複用時的殘留,不影響功能,但屬於可以清理的小瑕疵。
請求生命週期:一次完整的新增與編輯流程
把控制器、路由與資料層串起來,一次「新增博文」請求的完整路徑是:
- 瀏覽器 GET
/new→ 路由器比對/new規則 → 以反射建立NewController實例,執行Init → Prepare → Get()(見 13.2 的ServeHTTP分發邏輯); Get()設定Layout = "layout.tpl"與TplName = "new.tpl",Render()將表單頁嵌入佈局後輸出;- 使用者填表並 POST 到
/new→ 路由器依 method 改為執行Post(); Post()讀取表單、組裝Blog、呼叫models.SaveBlog寫庫,再Redirect(302, "/");- 瀏覽器跟隨跳轉 GET
/→IndexController.Get()呼叫GetAll()查出最新列表並渲染,新博文出現在首頁。
「編輯」流程同理,只是 GET 階段多了「依 id 查詢並預填」,POST 階段多了「讀取隱藏 id 走更新語義」。這與 13.1 專案規劃 中描述的框架資料流(入口初始化 → 路由分發 → 控制器裝載 model → 輸出 view)完全吻合,本節部落格就是該流程圖的一次具體落地。
執行前置條件與注意事項
在執行這個部落格範例前,需要準備:
- GOPATH 與專案目錄:依 13.1 專案規劃,設定
GOPATH並保證其下有src/pkg/bin三個目錄,部落格原始碼放在$GOPATH/src下; - 依賴庫:
go get github.com/astaxie/beedb與go get github.com/ziutek/mymysql,前者是 ORM,後者提供mymysql驅動; - 資料庫:依
blog/astaxie/123456建立名為blog的資料庫、使用者astaxie、密碼123456,並執行本節的CREATE TABLE entries建表語句; - 範本命名:控制器中
Layout、TplName均為相對於views目錄的檔名;13.3 提到框架預設範本擴展名TplExt = "tpl",本節的.tpl檔案與之匹配; - 自動渲染:確認
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
相关推荐
KeystoneJS测试体系搭建:单元测试与Nightwatch E2E自动化测试完整教程
KeystoneJS测试体系搭建:单元测试与Nightwatch E2E自动化测试完整教程 KeystoneJS 是一款基于 Node.js 的内容管理系统(C
文档教程IoT-For-Beginners 实战:使用 MQTT 发布 DHT11 温度遥测数据,为 GDD 积温计算铺路
IoT For Beginners 实战:使用 MQTT 发布 DHT11 温度遥测数据,为 GDD 积温计算铺路 本指南聚焦于 Azure IoT For B
文档教程Go 處理 JSON 全攻略:encoding/json 的解析、產生與 struct tag 實戰(build-web-application-with-golang 第 7.2 節)
Go 處理 JSON 全攻略:encoding/json 的解析、產生與 struct tag 實戰(build web application with go
文档教程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考