通用方法模块的设计
- 通用的方法可以定义在一个模块里,以方便调用,无需重复造轮子
- 一般可以设计一个顶层的 models 包来承载公共方法
models 包
package modelsimport ("crypto/md5""encoding/hex""math/rand""time""fmt""io"
)// 通用时间模板定义
var goTime = "2006-01-02 15:04:05"
var shortGoTime = "2006-01-02"
var md5Salt = GenerateSalt(16) // 加密时用的盐// 时间戳转换成日期
func UnixToTime(timestamp int) string {t := time.Unix(int64(timestamp), 0)return t.Format(goTime)
}// 日期转换成时间戳
func DateToUnix(str string) int64 {t, err := time.ParseInLocation(goTime, str, time.Local)if err != nil {return 0}return t.Unix()
}// 获取时间戳
func GetUnix() int64 {return time.Now().Unix()
}// 获取当前的日期
func GetDate() string {return time.Now().Format(goTime)
}// 获取年月日
func GetDateShort() string {return time.Now().Format(shortGoTime)
}// GenerateSalt 生成一个指定长度的随机盐
func GenerateSalt(length int) string {const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))b := make([]byte, length)for i := range b {b[i] = charset[seededRand.Intn(len(charset))]}return string(b)
}// MD5Hash 计算并返回给定字符串的 MD5 哈希值
func Md5Hash(text string) string {hasher := md5.New()hasher.Write([]byte(text + md5Salt))md5Bytes := hasher.Sum(nil)return hex.EncodeToString(md5Bytes)
}// 第二种加密方式,和上述结果一致
func Md5Hash2(text string) string {md5str := fmt.Sprintf("%x", md5.Sum([]byte(text + md5Salt)))return md5str
}// 第三种加密方法
func Md5Hash3(text string) string {hasher := md5.New()io.WriteString(hasher, text + md5Salt)md5str := fmt.Sprintf("%x", hasher.Sum(nil))return md5str
}// VerifyMD5Hash 验证给定的明文密码是否与存储的哈希值匹配
func VerifyMD5Hash(plainPassword, storedHash string) bool {return storedHash == Md5Hash(plainPassword)
}
- 上述的方法,可以写在程序里,通过models来调用,也可配置全局,在模板里调用
main.go 主程序
package mainimport ("gin-demo/routers" // gin-demo 是 go mod init 初始化的工程,下同"github.com/gin-gonic/gin""html/template""gin-demo/models"
)func main() {// 创建一个默认的路由引擎r := gin.Default()// 自定义模板函数 注意要把这个函数放在加载模板前 全局配置r.SetFuncMap(template.FuncMap{"UnixToTime": models.UnixToTime,"DateToUnix": models.DateToUnix,"GetUnix": models.GetUnix,"GetDate": models.GetDate,"GetDateShort": models.GetDateShort,})// 加载模板 放在配置路由前面r.LoadHTMLGlob("tpls/**/*")routers.WebRoutersInit(r)routers.ApiRoutersInit(r)routers.AdminRoutersInit(r)r.Run()
}
web首页控制器
package webimport ("gin-demo/models""net/http""github.com/gin-gonic/gin"
)type WebCtrl struct{}func (con WebCtrl) Index(c *gin.Context) {// 验证加密和验证originStr := "dsdssdsdsfdsdsfss" // 原字符md5HashStr := models.Md5Hash(originStr) // 加密后的字符串checkMd5Result := models.VerifyMD5Hash(originStr, md5HashStr) // 加密后的验证c.HTML(http.StatusOK, "web/index.html", gin.H{"msg": "我是一个msg","t": 1760025600,"d": "2025-10-10 00:00:00","originStr": originStr, // 原字符串"md5HashStr": md5HashStr, // md5 加密后的字符串"checkMd5Result": checkMd5Result, // 验证是否匹配"testMd5_2": models.Md5Hash2(originStr), // 第二种加密结果"testMd5_3": models.Md5Hash3(originStr), // 第三种加密结果})
}
web首页模板 tpls/web/index.html
{{ define "web/index.html" }}<h3>web index 页面</h3>{{.msg}}<h3> UnixToTime 时间处理</h3>{{ UnixToTime .t }} <br />{{ .t | UnixToTime }}<h3> DateToUnix 时间处理</h3>{{ .d | DateToUnix }} <br />{{ DateToUnix .d }}<h3> GetUnix 时间处理</h3>{{ GetUnix }} <br /><h3> GetDate 时间处理</h3>{{ GetDate }}<h3> GetUnix 时间处理</h3>{{ GetDateShort }}<h3> md5 原字符串 </h3>{{ .originStr }}<h3> md5 加密后的hash </h3>{{ .md5HashStr }}<h3> md5 验证是否相同 </h3>{{ .checkMd5Result }}<h3> md5 第二种md5加密方式得到的字符串 </h3>{{ .testMd5_2 }}<h3> md5 第三种md5加密方式得到的字符串 </h3>{{ .testMd5_3 }}{{ end }}
效果图
- 如果我们的应用非常简单的话,我们可以在 Controller 里面处理常见的业务逻辑
- 但是如果我们有一个功能想在多个控制器、或者多个模板里面复用的话
- 那么我们就可以把公共的功能单独抽取出来作为一个模块(Models)
- Model 是逐步抽象的过程,一般我们会在 Model
- 里面封装一些公共的方法让不同 Controller 使用
- 也可以在 Model 中实现和数据库打交道
- 以上是这些应用场景,和其他项目设计都是一致的思路
上传功能
这里上传分多种场景:上传单张图片,上传多张图片
在上传多张图片中又分为,同一文件字段和不同文件字段的处理
1 ) 设计路由
package routersimport ("gin-demo/controllers/admin""gin-demo/middlewares" // 引入"github.com/gin-gonic/gin"
)func AdminRoutersInit(r *gin.Engine) {adminRouters := r.Group("/admin", middlewares.InitMiddleware) // 注意这里{adminRouters.GET("/", admin.IndexCtrl{}.Index)adminRouters.GET("/user", admin.UserCtrl{}.Index)adminRouters.GET("/user/success", admin.UserCtrl{}.Success)adminRouters.GET("/user/error", admin.UserCtrl{}.Error)adminRouters.GET("/user/upload", admin.UserCtrl{}.Upload) // 这是单个上传页面adminRouters.GET("/user/uploads", admin.UserCtrl{}.Uploads) // 这个是多文件上传页面,在这个页面中有两个上传入口adminRouters.POST("/user/doUpload", admin.UserCtrl{}.DoUpload) // 处理单个上传adminRouters.POST("/user/doUploads", admin.UserCtrl{}.DoUploads) // 处理多个上传(同一字段)adminRouters.POST("/user/doUploads2", admin.UserCtrl{}.DoUploads2) // 处理多个上传(不同字段)}
}
2 ) main 文件中对上传路径和模板的处理
package mainimport ("gin-demo/routers" // gin-demo 是 go mod init 初始化的工程,下同"github.com/gin-gonic/gin""html/template""gin-demo/models"
)func main() {// 创建一个默认的路由引擎r := gin.Default()// 自定义模板函数 注意要把这个函数放在加载模板前 全局配置r.SetFuncMap(template.FuncMap{"UnixToTime": models.UnixToTime,"DateToUnix": models.DateToUnix,"GetUnix": models.GetUnix,"GetDate": models.GetDate,"GetDateShort": models.GetDateShort,})// 加载模板 放在配置路由前面r.LoadHTMLGlob("tpls/**/**/*")// 配置静态web目录 第一个参数表示路由, 第二个参数表示映射的目录r.Static("/static", "./static")routers.WebRoutersInit(r)routers.ApiRoutersInit(r)routers.AdminRoutersInit(r)r.Run()
}
注意了,这个之前的项目中,首页模板修改成 tpls/web/home/index.html 就不会有问题了 (这个可忽略,因为不涉及)
主要是这两个
// 加载模板 放在配置路由前面
r.LoadHTMLGlob("tpls/**/**/*")
// 配置静态web目录 第一个参数表示路由, 第二个参数表示映射的目录
r.Static("/static", "./static")
3 )HTML 模板文件处理
3.1 单个上传
{{ define "admin/user/upload.html" }}<h2>演示单一文件上传</h2><form action="/admin/user/doUpload" method="post" enctype="multipart/form-data">用户名:<input type="text" name="username" placeholder="用户名" /><br><br>头 像<input type="file" name="face" /><br> <br><input type="submit" value="提交"></form>{{ end }}
3.2 多个上传
{{ define "admin/user/uploads.html" }}<h2>演示多文件上传:相同文件字段</h2><form action="/admin/user/doUploads" method="post" enctype="multipart/form-data">用户名:<input type="text" name="username" placeholder="用户名" /><br><br>头 像1:<input type="file" name="face[]" /><br> <br>头 像2:<input type="file" name="face[]" /><br> <br>头 像3:<input type="file" name="face[]" /><br> <br><input type="submit" value="提交"></form><br /><hr /><br /><h2>演示多文件上传:不同的文件字段</h2><form action="/admin/user/doUploads2" method="post" enctype="multipart/form-data">用户名:<input type="text" name="username" placeholder="用户名" /><br><br>头 像1:<input type="file" name="face1" /><br> <br>头 像2:<input type="file" name="face2" /><br> <br><input type="submit" value="提交"></form>{{ end }}
4 ) 上传控制器
package adminimport ("gin-demo/models""mime/multipart""net/http""os""path""strconv""time""math/rand""github.com/gin-gonic/gin"
)type UserCtrl struct {BaseCtrl
}// 获取 单一上传 页面
func (con UserCtrl) Upload(c *gin.Context) {c.HTML(http.StatusOK, "admin/user/upload.html", gin.H{})
}// 获取 多上传 页面
func (con UserCtrl) Uploads(c *gin.Context) {c.HTML(http.StatusOK, "admin/user/uploads.html", gin.H{})
}// 允许的后缀判断
func checkAllowedExt(extName string, c *gin.Context) bool {allowExtMap := map[string]bool{ ".jpg": true, ".png": true, ".gif": true, ".jpeg": true, }if _, ok := allowExtMap[extName]; !ok {return false}return true
}// 获取文件路径
func saveFile(file *multipart.FileHeader, c *gin.Context) string {fileName := file.FilenameextName := path.Ext(fileName) // 获取后缀// 判断文件后缀名是否正确 .jpg .png .gif .jpegif (!checkAllowedExt(extName, c)) {c.String(200, "文件类型不合法")return ""}// 处理文件存储路径day := models.GetDateShort()dir := "./static/upload/" + day// 0777用来测试 生产环境应使用 0666 让其不可执行,保证安全性if err := os.MkdirAll(dir, 0777); err != nil {// 处理错误return ""}source := rand.NewSource(time.Now().UnixNano()) // 初始化随机数种子rnd := rand.New(source) // 使用新的随机数源创建一个随机数生成器random10DigitNumber := rnd.Intn(9000000000) + 1000000000 // 生成一个10位的随机整数 // 没有错误 生成文件名称 144325235235_xxxxxxxxxx.pngfileUnixName := strconv.FormatInt(models.GetUnix(), 10) + "-" + strconv.Itoa(random10DigitNumber)//static/upload/20200623/144325235235.pngsavedDir := path.Join(dir, fileUnixName + extName)// 存储c.SaveUploadedFile(file, savedDir)return savedDir
}// 处理 单一上传 提交
func (con UserCtrl) DoUpload(c *gin.Context) {// 获取表单username := c.PostForm("username") // 获取字段file, err := c.FormFile("face") // 获取文件if err != nil {// 处理错误c.String(http.StatusOK, "文件错误:%v", err)return}// 保存文件位置dst := saveFile(file, c)if dst == "" {return}// 响应jsonc.JSON(http.StatusOK, gin.H{"success": true,"username": username,"dst": dst,})
}// 处理 多上传 提交:相同字段
func (con UserCtrl) DoUploads(c *gin.Context) {// 获取表单username := c.PostForm("username")form, _ := c.MultipartForm()// 处理文件files := form.File["face[]"]// 没有文件的校验if (len(files) == 0) {// 做一些处理c.String(http.StatusOK, "没上传一张图片!")return}var dsts []string// 处理文件for _, file := range files {// 进入存储流程dst := saveFile(file, c)if (dst == "") {continue}dsts = append(dsts, dst)}c.JSON(http.StatusOK, gin.H{"success": true,"username": username,"dst": dsts,})
}// 处理 多上传 提交:不同字段
func (con UserCtrl) DoUploads2(c *gin.Context) {// 表单处理username := c.PostForm("username")file1, err1 := c.FormFile("face1")file2, err2 := c.FormFile("face2")// 错误处理:上传文件到指定的目录if !(err1 == nil || err2 == nil) {c.String(200, "没有上传文件")return;}// 保存文件var dsts []string// 处理文件if file1 != nil {dst1 := saveFile(file1, c)if (dst1 != "") {dsts = append(dsts, dst1)}}if file2 != nil {dst2 := saveFile(file2, c)if (dst2 != "") {dsts = append(dsts, dst2)}}// 响应c.JSON(http.StatusOK, gin.H{"success": true,"username": username,"dst": dsts,})
}
5 ) 聊聊注意事项
- 上传文件注意 html 模板中设置
enctype="multipart/form-data"
这个是关键 - 上传时,如果有文件服务器,需要上传到文件服务器中,这里只演示本服务器上传基础功能
- 上传图片时的路径处理,包括按照时间来创建目录和文件
- 这里仅是一个示例程序,用于学习和交流,可以基于此来改造