原文地址:優化配置結構及實現圖片上傳
項目地址:https://github.com/EDDYCJY/go...html
若是對你有所幫助,歡迎點個 Star 👍前端
一天,產品經理忽然跟你說文章列表,沒有封面圖,不夠美觀,!)&¥!&)#&¥!加一個吧,幾分鐘的事mysql
你打開你的程序,分析了一波寫了個清單:git
嗯,你發現要較優的話,須要調整部分的應用程序結構,由於功能愈來愈多,本來的設計也要跟上節奏github
也就是在適當的時候,及時優化golang
在先前章節中,採用了直接讀取 KEY 的方式去存儲配置項,而本次需求中,須要增長圖片的配置項,整體就有些冗餘了sql
咱們採用如下解決方法:數據庫
在 go-ini 中能夠採用 MapTo 的方式來映射結構體,例如:segmentfault
type Server struct { RunMode string HttpPort int ReadTimeout time.Duration WriteTimeout time.Duration } var ServerSetting = &Server{} func main() { Cfg, err := ini.Load("conf/app.ini") if err != nil { log.Fatalf("Fail to parse 'conf/app.ini': %v", err) } err = Cfg.Section("server").MapTo(ServerSetting) if err != nil { log.Fatalf("Cfg.MapTo ServerSetting err: %v", err) } }
在這段代碼中,能夠注意 ServerSetting 取了地址,爲何 MapTo 必須地址入參呢?api
// MapTo maps section to given struct. func (s *Section) MapTo(v interface{}) error { typ := reflect.TypeOf(v) val := reflect.ValueOf(v) if typ.Kind() == reflect.Ptr { typ = typ.Elem() val = val.Elem() } else { return errors.New("cannot map to non-pointer struct") } return s.mapTo(val, false) }
在 MapTo 中 typ.Kind() == reflect.Ptr
約束了必須使用指針,不然會返回 cannot map to non-pointer struct
的錯誤。這個是表面緣由
更往內探究,能夠認爲是 field.Set
的緣由,當執行 val := reflect.ValueOf(v)
,函數經過傳遞 v
拷貝建立了 val
,可是 val
的改變並不能更改原始的 v
,要想 val
的更改能做用到 v
,則必須傳遞 v
的地址
顯然 go-ini 裏也是包含修改原始值這一項功能的,你以爲是什麼緣由呢?
在先前的版本中,models 和 file 的配置是在本身的文件中解析的,而其餘在 setting.go 中,所以咱們須要將其在 setting 中統一接管
你可能會想,直接把二者的配置項複製粘貼到 setting.go 的 init 中,一會兒就完事了,搞那麼麻煩?
但你在想一想,先前的代碼中存在多個 init 函數,執行順序存在問題,沒法達到咱們的要求,你能夠試試
(此處是一個基礎知識點)
在 Go 中,當存在多個 init 函數時,執行順序爲:
因此要避免多 init 的狀況,儘可能由程序把控初始化的前後順序
打開 conf/app.ini 將配置文件修改成大駝峯命名,另外咱們增長了 5 個配置項用於上傳圖片的功能,4 個文件日誌方面的配置項
[app] PageSize = 10 JwtSecret = 233 RuntimeRootPath = runtime/ ImagePrefixUrl = http://127.0.0.1:8000 ImageSavePath = upload/images/ # MB ImageMaxSize = 5 ImageAllowExts = .jpg,.jpeg,.png LogSavePath = logs/ LogSaveName = log LogFileExt = log TimeFormat = 20060102 [server] #debug or release RunMode = debug HttpPort = 8000 ReadTimeout = 60 WriteTimeout = 60 [database] Type = mysql User = root Password = rootroot Host = 127.0.0.1:3306 Name = blog TablePrefix = blog_
將散落在其餘文件裏的配置都刪掉,統一在 setting 中處理以及修改 init 函數爲 Setup 方法
打開 pkg/setting/setting.go 文件,修改以下:
package setting import ( "log" "time" "github.com/go-ini/ini" ) type App struct { JwtSecret string PageSize int RuntimeRootPath string ImagePrefixUrl string ImageSavePath string ImageMaxSize int ImageAllowExts []string LogSavePath string LogSaveName string LogFileExt string TimeFormat string } var AppSetting = &App{} type Server struct { RunMode string HttpPort int ReadTimeout time.Duration WriteTimeout time.Duration } var ServerSetting = &Server{} type Database struct { Type string User string Password string Host string Name string TablePrefix string } var DatabaseSetting = &Database{} func Setup() { Cfg, err := ini.Load("conf/app.ini") if err != nil { log.Fatalf("Fail to parse 'conf/app.ini': %v", err) } err = Cfg.Section("app").MapTo(AppSetting) if err != nil { log.Fatalf("Cfg.MapTo AppSetting err: %v", err) } AppSetting.ImageMaxSize = AppSetting.ImageMaxSize * 1024 * 1024 err = Cfg.Section("server").MapTo(ServerSetting) if err != nil { log.Fatalf("Cfg.MapTo ServerSetting err: %v", err) } ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second ServerSetting.WriteTimeout = ServerSetting.ReadTimeout * time.Second err = Cfg.Section("database").MapTo(DatabaseSetting) if err != nil { log.Fatalf("Cfg.MapTo DatabaseSetting err: %v", err) } }
在這裏,咱們作了以下幾件事:
須要你去作的事:
這幾項比較基礎,並無貼出來,我但願你能夠本身動手,有問題的話可右拐 項目地址
在這一步咱們要設置初始化的流程,打開 main.go 文件,修改內容:
func main() { setting.Setup() models.Setup() logging.Setup() endless.DefaultReadTimeOut = setting.ServerSetting.ReadTimeout endless.DefaultWriteTimeOut = setting.ServerSetting.WriteTimeout endless.DefaultMaxHeaderBytes = 1 << 20 endPoint := fmt.Sprintf(":%d", setting.ServerSetting.HttpPort) server := endless.NewServer(endPoint, routers.InitRouter()) server.BeforeBegin = func(add string) { log.Printf("Actual pid is %d", syscall.Getpid()) } err := server.ListenAndServe() if err != nil { log.Printf("Server err: %v", err) } }
修改完畢後,就成功將多模塊的初始化函數放到啓動流程中了(前後順序也能夠控制)
在這裏爲止,針對本需求的配置優化就完畢了,你須要執行 go run main.go
驗證一下你的功能是否正常哦
順帶留個基礎問題,你們能夠思考下
ServerSetting.ReadTimeout = ServerSetting.ReadTimeout * time.Second ServerSetting.WriteTimeout = ServerSetting.ReadTimeout * time.Second
若將 setting.go 文件中的這兩行刪除,會出現什麼問題,爲何呢?
在先前版本中,在 logging/file.go 中使用到了 os 的一些方法,咱們經過前期規劃發現,這部分在上傳圖片功能中能夠複用
在 pkg 目錄下新建 file/file.go ,寫入文件內容以下:
package file import ( "os" "path" "mime/multipart" "io/ioutil" ) func GetSize(f multipart.File) (int, error) { content, err := ioutil.ReadAll(f) return len(content), err } func GetExt(fileName string) string { return path.Ext(fileName) } func CheckExist(src string) bool { _, err := os.Stat(src) return os.IsNotExist(err) } func CheckPermission(src string) bool { _, err := os.Stat(src) return os.IsPermission(err) } func IsNotExistMkDir(src string) error { if exist := CheckExist(src); exist == false { if err := MkDir(src); err != nil { return err } } return nil } func MkDir(src string) error { err := os.MkdirAll(src, os.ModePerm) if err != nil { return err } return nil } func Open(name string, flag int, perm os.FileMode) (*os.File, error) { f, err := os.OpenFile(name, flag, perm) if err != nil { return nil, err } return f, nil }
在這裏咱們一共封裝了 7個 方法
在這裏咱們用到了 mime/multipart
包,它主要實現了 MIME 的 multipart 解析,主要適用於 HTTP 和常見瀏覽器生成的 multipart 主體
multipart 又是什麼,rfc2388 的 multipart/form-data 瞭解一下
咱們在第一步已經將 file 從新封裝了一層,在這一步咱們將原先 logging 包的方法都修改掉
一、打開 pkg/logging/file.go 文件,修改文件內容:
package logging import ( "fmt" "os" "time" "github.com/EDDYCJY/go-gin-example/pkg/setting" "github.com/EDDYCJY/go-gin-example/pkg/file" ) func getLogFilePath() string { return fmt.Sprintf("%s%s", setting.AppSetting.RuntimeRootPath, setting.AppSetting.LogSavePath) } func getLogFileName() string { return fmt.Sprintf("%s%s.%s", setting.AppSetting.LogSaveName, time.Now().Format(setting.AppSetting.TimeFormat), setting.AppSetting.LogFileExt, ) } func openLogFile(fileName, filePath string) (*os.File, error) { dir, err := os.Getwd() if err != nil { return nil, fmt.Errorf("os.Getwd err: %v", err) } src := dir + "/" + filePath perm := file.CheckPermission(src) if perm == true { return nil, fmt.Errorf("file.CheckPermission Permission denied src: %s", src) } err = file.IsNotExistMkDir(src) if err != nil { return nil, fmt.Errorf("file.IsNotExistMkDir src: %s, err: %v", src, err) } f, err := file.Open(src + fileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return nil, fmt.Errorf("Fail to OpenFile :%v", err) } return f, nil }
咱們將引用都改成了 file/file.go 包裏的方法
二、打開 pkg/logging/log.go 文件,修改文件內容:
package logging ... func Setup() { var err error filePath := getLogFilePath() fileName := getLogFileName() F, err = openLogFile(fileName, filePath) if err != nil { log.Fatalln(err) } logger = log.New(F, DefaultPrefix, log.LstdFlags) } ...
因爲原方法形參改變了,所以 openLogFile 也須要調整
這一小節,咱們開始實現上次圖片相關的一些方法和功能
首先須要在 blog_article 中增長字段 cover_image_url
,格式爲 varchar(255) DEFAULT '' COMMENT '封面圖片地址'
通常不會直接將上傳的圖片名暴露出來,所以咱們對圖片名進行 MD5 來達到這個效果
在 util 目錄下新建 md5.go,寫入文件內容:
package util import ( "crypto/md5" "encoding/hex" ) func EncodeMD5(value string) string { m := md5.New() m.Write([]byte(value)) return hex.EncodeToString(m.Sum(nil)) }
在先前咱們已經把底層方法給封裝好了,實質這一步爲封裝 image 的處理邏輯
在 pkg 目錄下新建 upload/image.go 文件,寫入文件內容:
package upload import ( "os" "path" "log" "fmt" "strings" "mime/multipart" "github.com/EDDYCJY/go-gin-example/pkg/file" "github.com/EDDYCJY/go-gin-example/pkg/setting" "github.com/EDDYCJY/go-gin-example/pkg/logging" "github.com/EDDYCJY/go-gin-example/pkg/util" ) func GetImageFullUrl(name string) string { return setting.AppSetting.ImagePrefixUrl + "/" + GetImagePath() + name } func GetImageName(name string) string { ext := path.Ext(name) fileName := strings.TrimSuffix(name, ext) fileName = util.EncodeMD5(fileName) return fileName + ext } func GetImagePath() string { return setting.AppSetting.ImageSavePath } func GetImageFullPath() string { return setting.AppSetting.RuntimeRootPath + GetImagePath() } func CheckImageExt(fileName string) bool { ext := file.GetExt(fileName) for _, allowExt := range setting.AppSetting.ImageAllowExts { if strings.ToUpper(allowExt) == strings.ToUpper(ext) { return true } } return false } func CheckImageSize(f multipart.File) bool { size, err := file.GetSize(f) if err != nil { log.Println(err) logging.Warn(err) return false } return size <= setting.AppSetting.ImageMaxSize } func CheckImage(src string) error { dir, err := os.Getwd() if err != nil { return fmt.Errorf("os.Getwd err: %v", err) } err = file.IsNotExistMkDir(dir + "/" + src) if err != nil { return fmt.Errorf("file.IsNotExistMkDir err: %v", err) } perm := file.CheckPermission(src) if perm == true { return fmt.Errorf("file.CheckPermission Permission denied src: %s", src) } return nil }
在這裏咱們實現了 7 個方法,以下:
這裏基本是對底層代碼的二次封裝,爲了更靈活的處理一些圖片特有的邏輯,而且方便修改,不直接對外暴露下層
這一步將編寫上傳圖片的業務邏輯,在 routers/api 目錄下 新建 upload.go 文件,寫入文件內容:
package api import ( "net/http" "github.com/gin-gonic/gin" "github.com/EDDYCJY/go-gin-example/pkg/e" "github.com/EDDYCJY/go-gin-example/pkg/logging" "github.com/EDDYCJY/go-gin-example/pkg/upload" ) func UploadImage(c *gin.Context) { code := e.SUCCESS data := make(map[string]string) file, image, err := c.Request.FormFile("image") if err != nil { logging.Warn(err) code = e.ERROR c.JSON(http.StatusOK, gin.H{ "code": code, "msg": e.GetMsg(code), "data": data, }) } if image == nil { code = e.INVALID_PARAMS } else { imageName := upload.GetImageName(image.Filename) fullPath := upload.GetImageFullPath() savePath := upload.GetImagePath() src := fullPath + imageName if ! upload.CheckImageExt(imageName) || ! upload.CheckImageSize(file) { code = e.ERROR_UPLOAD_CHECK_IMAGE_FORMAT } else { err := upload.CheckImage(fullPath) if err != nil { logging.Warn(err) code = e.ERROR_UPLOAD_CHECK_IMAGE_FAIL } else if err := c.SaveUploadedFile(image, src); err != nil { logging.Warn(err) code = e.ERROR_UPLOAD_SAVE_IMAGE_FAIL } else { data["image_url"] = upload.GetImageFullUrl(imageName) data["image_save_url"] = savePath + imageName } } } c.JSON(http.StatusOK, gin.H{ "code": code, "msg": e.GetMsg(code), "data": data, }) }
所涉及的錯誤碼(需在 pkg/e/code.go、msg.go 添加):
// 保存圖片失敗 ERROR_UPLOAD_SAVE_IMAGE_FAIL = 30001 // 檢查圖片失敗 ERROR_UPLOAD_CHECK_IMAGE_FAIL = 30002 // 校驗圖片錯誤,圖片格式或大小有問題 ERROR_UPLOAD_CHECK_IMAGE_FORMAT = 30003
在這一大段的業務邏輯中,咱們作了以下事情:
總的來講,就是 入參 -> 檢查 -》 保存 的應用流程
打開 routers/router.go 文件,增長路由 r.POST("/upload", api.UploadImage)
,如:
func InitRouter() *gin.Engine { r := gin.New() ... r.GET("/auth", api.GetAuth) r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) r.POST("/upload", api.UploadImage) apiv1 := r.Group("/api/v1") apiv1.Use(jwt.JWT()) { ... } return r }
最後咱們請求一下上傳圖片的接口,測試所編寫的功能
檢查目錄下是否含文件(注意權限問題)
$ pwd $GOPATH/src/github.com/EDDYCJY/go-gin-example/runtime/upload/images $ ll ... 96a3be3cf272e017046d1b2674a52bd3.jpg ... c39fa784216313cf2faa7c98739fc367.jpeg
在這裏咱們一共返回了 2 個參數,一個是完整的訪問 URL,另外一個爲保存路徑
在完成了上一小節後,咱們還須要讓前端可以訪問到圖片,通常是以下:
在公司的話,CDN 或自建分佈式文件系統居多,也不須要過多關注。而在實踐裏的話確定是本地搭建了,Go 自己對此就有很好的支持,而 Gin 更是再封裝了一層,只須要在路由增長一行代碼便可
打開 routers/router.go 文件,增長路由 r.StaticFS("/upload/images", http.Dir(upload.GetImageFullPath()))
,如:
func InitRouter() *gin.Engine { ... r.StaticFS("/upload/images", http.Dir(upload.GetImageFullPath())) r.GET("/auth", api.GetAuth) r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler)) r.POST("/upload", api.UploadImage) ... }
當訪問 $HOST/upload/images 時,將會讀取到 $GOPATH/src/github.com/EDDYCJY/go-gin-example/runtime/upload/images 下的文件
而這行代碼又作了什麼事呢,咱們來看看方法原型
// StaticFS works just like `Static()` but a custom `http.FileSystem` can be used instead. // Gin by default user: gin.Dir() func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes { if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { panic("URL parameters can not be used when serving a static folder") } handler := group.createStaticHandler(relativePath, fs) urlPattern := path.Join(relativePath, "/*filepath") // Register GET and HEAD handlers group.GET(urlPattern, handler) group.HEAD(urlPattern, handler) return group.returnObj() }
首先在暴露的 URL 中禁止了 * 和 : 符號的使用,經過 createStaticHandler
建立了靜態文件服務,實質最終調用的仍是 fileServer.ServeHTTP
和一些處理邏輯了
func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { absolutePath := group.calculateAbsolutePath(relativePath) fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) _, nolisting := fs.(*onlyfilesFS) return func(c *Context) { if nolisting { c.Writer.WriteHeader(404) } fileServer.ServeHTTP(c.Writer, c.Request) } }
咱們能夠留意下 fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
這段語句,在靜態文件服務中很常見,它有什麼做用呢?
http.StripPrefix
主要做用是從請求 URL 的路徑中刪除給定的前綴,最終返回一個 Handler
一般 http.FileServer 要與 http.StripPrefix 相結合使用,不然當你運行:
http.Handle("/upload/images", http.FileServer(http.Dir("upload/images")))
會沒法正確的訪問到文件目錄,由於 /upload/images
也包含在了 URL 路徑中,必須使用:
http.Handle("/upload/images", http.StripPrefix("upload/images", http.FileServer(http.Dir("upload/images"))))
到下面能夠看到 urlPattern := path.Join(relativePath, "/*filepath")
,/*filepath
你是誰,你在這裏有什麼用,你是 Gin 的產物嗎?
經過語義可得知是路由的處理邏輯,而 Gin 的路由是基於 httprouter 的,經過查閱文檔可獲得如下信息
Pattern: /src/*filepath /src/ match /src/somefile.go match /src/subdir/somefile.go match
*filepath
將匹配全部文件路徑,而且 *filepath
必須在 Pattern 的最後
從新執行 go run main.go
,去訪問剛剛在 upload 接口獲得的圖片地址,檢查 http.FileSystem 是否正常
接下來,須要你修改 routers/api/v1/article.go 的 AddArticle、EditArticle 兩個接口
這塊前面文章講過,若是有問題能夠參考項目的代碼👌
在這章節中,咱們簡單的分析了下需求,對應用作出了一個小規劃並實施
完成了清單中的功能點和優化,在實際項目中也是常見的場景,但願你可以細細品嚐並針對一些點進行深刻學習