原项目为 java 编写的 jpetStore,原 java 版:https://blog.csdn.net/qq_39446719/article/details/80821440
现改为使用 go 语言编写,旨在上手 go web 编程,github:https://github.com/SwordHarry/gopetstore
template模板渲染 + go + mysql
没有使用web框架,围绕 go http标准库,旨在上手 go web 编程
采用 MVC 分层开发:DAO-persistence、service、controller、template
使用 gorilla/sessions 等第三方库进行集成迭代
这里列出在这个项目中从 java 到 go 需要重新学习和踩坑的点
go 自带 template 库进行模板渲染,其中在本次开发中遇到和需要注意的点有:
传给模板的结构体属性名需要大写,不然非导出,模板获取不到值
模板中没有按照指定索引从列表中获取值的方法,如果需要,可以自己进行函数编写和注册
在进行模板渲染时,第一个传入的文件路径是主文件模板。要按照模板中出现的顺序进行传参和解析
t := template.Must(template.ParseFiles(fileNames...))
这里的 fileNames 需要严格按照解析顺序进行传参,否则会造成白屏
这里列出在项目中常用的模板语法
{{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}}
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
}
scan 解析时将会报错,可以在 SQL 中使用 IFNULL sql 函数,如果为 null,则取默认值
IFNULL(username, "")
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 始终是值传递,关于 赋值的时候就会创建对象副本,可以详细参考文章:[]T 还是 []*T, 这是一个问题
一般的判断标准是看副本创建的成本和需求。
T
。相反,如果想修改原始的变量,则选择*T
*T
,只创建新的指针,这个区别是巨大的T
,Go编译器尽量将对象分配到栈上,而*T
很可能会分配到堆上,这对垃圾回收会有影响赋值的时候就会创建对象副本
最常见的赋值的例子是对变量的赋值,包括函数内和函数外
T
类型的变量和*T
类型的变量在当做函数或者方法的参数时会传递它的副本
slice,map和数组在初始化和按索引设置的时候会创建副本
for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本
往channel中send对象的时候也会创建对象的副本
函数或方法的参数和返回值
方法接收者本身也是一个副本
以上是 go web 无框架编程期间遇到的问题和学习历程,没有使用到 web 框架,旨在上手 go web 编程,接下来将使用 gin 和 gorm 对该项目进行再重构