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 等第三方库进行集成迭代

架构图

goPetStore: 基于go的无框架web项目_第1张图片

从 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 对该项目进行再重构

你可能感兴趣的:(go,jpetstore,go)