goPetStore: 基於go的無框架web項目
前言
原項目爲 java 編寫的 jpetStore,原 java 版:https://blog.csdn.net/qq_39446719/article/details/80821440
現改爲使用 go 語言編寫,旨在上手 go web 編程,github:https://github.com/SwordHarry/gopetstore
業務模塊
- 商品模塊
- category
- product
- item
- search
- 購物車模塊
- cart
- 用戶模塊
- account
- 訂單模塊
- order
- lineItem
- sequence
架構
template模板渲染 + go + mysql
沒有使用web框架,圍繞 go http標準庫,旨在上手 go web 編程
採用 MVC 分層開發:DAO-persistence、service、controller、template
使用 gorilla/sessions 等第三方庫進行集成迭代
架構圖
從 java 到 go 的陣痛
這裏列出在這個項目中從 java 到 go 需要重新學習和踩坑的點
html/template 庫的學習和踩坑
go 自帶 template 庫進行模板渲染,其中在本次開發中遇到和需要注意的點有:
-
傳給模板的結構體屬性名需要大寫,不然非導出,模板獲取不到值
-
模板中沒有按照指定索引從列表中獲取值的方法,如果需要,可以自己進行函數編寫和註冊
-
在進行模板渲染時,第一個傳入的文件路徑是主文件模板。要按照模板中出現的順序進行傳參和解析
t := template.Must(template.ParseFiles(fileNames...))
這裏的 fileNames 需要嚴格按照解析順序進行傳參,否則會造成白屏
go html/template 語法
這裏列出在項目中常用的模板語法
模板嵌套
{{define "header"}}
<div>...</div>
{{end}}
//使用
{{template "header" .}}
判斷
{{if and .condition1 .condition2}}
{{end}}
循環
{{range .ProductList}}
<li>{{.ProductId}}</li>
// 如果需要在循環內獲取循環外的屬性需要使用 $
<p>{{$.Account}}</p>
{{end}}
格式化輸出
{{printf "%.2f" .Item.ListPrice}}
// 格式化兩位小數
{{.Date.Format "2006-01-02"}}
// 日期格式化
函數調用
{{.Cart.Method}}
判斷是否爲空
判斷是否爲空:可以直接內嵌到某個屬性裏
<input type="text" name="firstName" value="{{if .Account}}{{.Account.FirstName}}{{end}}"/>
自定義函數的註冊和使用
自定義函數,當將 html 片段輸出到模板中時,瀏覽器默認不會進行解析,需要將 string 類型轉換成 template.HTML 類型
func Render(w http.ResponseWriter, data interface{}, fileNames ...string) error {
_, f := filepath.Split(fileNames[0])
// 這裏傳入的 New 中的文件名需要和模板的文件名一致
// 鏈式調用,註冊 html 片段解析函數
t, err := template.New(f).
Funcs(template.FuncMap{"unEscape": UnEscape}).
ParseFiles(fileNames...)
if t != nil {
return t.Execute(w, data)
}
return err
}
// 將html片段完整輸出並要求解析
func UnEscape(s string) template.HTML {
return template.HTML(s)
}
// 使用
{{.Description | unEscape}}
go 連接mysql數據庫
package util
import (
"database/sql"
"errors"
"log"
// 驅動需要進行隱式導入
_ "github.com/go-sql-driver/mysql"
)
const (
userName = "root"
password = "root"
dbName = "gopetstore"
driverName = "mysql"
charset = "charset=utf8"
local = "loc=Local"
tcpPort = "@tcp(localhost:3306)/"
parseTime = "parseTime=true" // 用以解析 數據庫 中的 date 類型,否則會解析成 []uint8 不能隱式轉爲 string
)
// 連接數據庫 mysql
func GetConnection() (*sql.DB, error) {
dataSourceName := userName + ":" + password + tcpPort + dbName + "?" + charset + "&" + local + "&" + parseTime
db, err := sql.Open(driverName, dataSourceName) //對應數據庫的用戶名和密碼以及數據庫名
return db, err
}
數據庫中出現 null
scan 解析時將會報錯,可以在 SQL 中使用 IFNULL sql 函數,如果爲 null,則取默認值
IFNULL(username, "")
go 使用 session 存儲用戶信息
go 標準庫中沒有session,故需要自己實現封裝或採用第三方庫。這裏使用gorilla/sessions庫進行集成迭代和再次封裝。文檔官網:http://www.gorillatoolkit.org/pkg/sessions
注意點
-
基本數據類型等可以直接存儲到session中,但是結構體等類型需要先使用 gob.Register 進行序列化註冊
package domain import ( "encoding/gob" ) type Product struct { ProductId string CategoryId string Name string Description string } // 序列化註冊 product,用於 session 存儲 func init() { gob.Register(&Product{}) }
-
最好採用 FileSystemStore,可以設置最大長度;而 CookieStore 即使設置了最大長度也依託瀏覽器限制;提前設置 setMaxLen,默認 4096, 容易超
-
在session 的保存和刪除數據之後,都需要進行一次 Save 操作,否則保存和刪除無效
/*
對 sessions 庫的再封裝,實現簡單session功能
*/
// 不暴露,保證 session 的單例
type session struct {
se *sessions.Session
}
// 祕鑰,生成唯一 sessionStore
const secretKey = "go-pet-store"
// go web 標準庫沒有 session,需要自己開發封裝或使用第三方的庫
var sessionStore = sessions.NewFilesystemStore("", []byte(secretKey))
const sessionName = "session"
// 初始化,通過這個獲取唯一 session
func GetSession(r *http.Request) (*session, error) {
// 設置 fileSystemStore 的最大存儲長度,防止溢出
sessionStore.MaxLength(5 * 4096)
s, err := sessionStore.Get(r, sessionName)
if err != nil {
return nil, err
}
return &session{
s,
}, nil
}
// 存儲和更新,複雜類型存儲前需要 gob.Register 進行序列化
func (s *session) Save(key string, val interface{}, w http.ResponseWriter, r *http.Request) error {
s.se.Values[key] = val
return s.se.Save(r, w)
}
// 獲取值
func (s *session) Get(key string) (result interface{}, ok bool) {
result, ok = s.se.Values[key]
return
}
// 刪除值
func (s *session) Del(key string, w http.ResponseWriter, r *http.Request) error {
delete(s.se.Values, key)
return s.se.Save(r, w) // 刪除之後也不忘進行 Save 操作
}
有條件可以使用 redis 作爲 session 中間件
go 值傳遞
go 始終是值傳遞,關於 賦值的時候就會創建對象副本,可以詳細參考文章:[]T 還是 []*T, 這是一個問題
如何選擇 T 或 *T
一般的判斷標準是看副本創建的成本和需求。
- 不想變量被修改。 如果你不想變量被函數和方法所修改,那麼選擇類型
T
。相反,如果想修改原始的變量,則選擇*T
- 如果變量是一個大的struct或者數組,則副本的創建相對會影響性能,這個時候考慮使用
*T
,只創建新的指針,這個區別是巨大的 - (不針對函數參數,只針對本地變量/本地變量)對於函數作用域內的參數,如果定義成
T
,Go編譯器儘量將對象分配到棧上,而*T
很可能會分配到堆上,這對垃圾回收會有影響
什麼時候發生副本創建
賦值的時候就會創建對象副本
-
最常見的賦值的例子是對變量的賦值,包括函數內和函數外
-
T
類型的變量和*T
類型的變量在當做函數或者方法的參數時會傳遞它的副本 -
slice,map和數組在初始化和按索引設置的時候會創建副本
-
for-range循環也是將元素的副本賦值給循環變量,所以變量得到的是集合元素的副本
-
往channel中send對象的時候也會創建對象的副本
-
函數或方法的參數和返回值
-
方法接收者本身也是一個副本
總結
以上是 go web 無框架編程期間遇到的問題和學習歷程,沒有使用到 web 框架,旨在上手 go web 編程,接下來將使用 gin 和 gorm 對該項目進行再重構