goPetStore: 基於go的無框架web項目

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 庫進行模板渲染,其中在本次開發中遇到和需要注意的點有:

  1. 傳給模板的結構體屬性名需要大寫,不然非導出,模板獲取不到值

  2. 模板中沒有按照指定索引從列表中獲取值的方法,如果需要,可以自己進行函數編寫和註冊

  3. 在進行模板渲染時,第一個傳入的文件路徑是主文件模板。要按照模板中出現的順序進行傳參和解析

    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

注意點

  1. 基本數據類型等可以直接存儲到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{})
    }
    
  2. 最好採用 FileSystemStore,可以設置最大長度;而 CookieStore 即使設置了最大長度也依託瀏覽器限制;提前設置 setMaxLen,默認 4096, 容易超

  3. 在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

一般的判斷標準是看副本創建的成本和需求。

  1. 不想變量被修改。 如果你不想變量被函數和方法所修改,那麼選擇類型T。相反,如果想修改原始的變量,則選擇*T
  2. 如果變量是一個的struct或者數組,則副本的創建相對會影響性能,這個時候考慮使用*T,只創建新的指針,這個區別是巨大的
  3. (不針對函數參數,只針對本地變量/本地變量)對於函數作用域內的參數,如果定義成T,Go編譯器儘量將對象分配到棧上,而*T很可能會分配到堆上,這對垃圾回收會有影響

什麼時候發生副本創建

賦值的時候就會創建對象副本

  • 最常見的賦值的例子是對變量的賦值,包括函數內和函數外

  • T類型的變量和*T類型的變量在當做函數或者方法的參數時會傳遞它的副本

  • slice,map和數組在初始化和按索引設置的時候會創建副本

  • for-range循環也是將元素的副本賦值給循環變量,所以變量得到的是集合元素的副本

  • 往channel中send對象的時候也會創建對象的副本

  • 函數或方法的參數和返回值

  • 方法接收者本身也是一個副本

總結

以上是 go web 無框架編程期間遇到的問題和學習歷程,沒有使用到 web 框架,旨在上手 go web 編程,接下來將使用 gin 和 gorm 對該項目進行再重構

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章