转载

Go Web App Example – Entry Point, File Structure, Models, and Routes

有很多可以快速搭建Go web项目的开源框架,与其用一个开源框架,我更愿意自己Go的原生的东西去构建一个带认证功能的model-view-controller (MVC) web 程序。记住,这只是众多构建你web 项目方法的一种。

可以在Github查看项目的代码: https://github.com/josephspurrier/gowebapp

项目文件结构

config/       - application settings and database schema controller/   - page logic organized by HTTP methods (GET, POST) model/        - database queries route/        - route information and middleware shared/       - packages for templates, MySQL, cryptography, sessions, and json static/       - location of statically served files like CSS and JS template/     - HTML templates gowebapp.db   - SQLite database gowebapp.go   - application entry point
config/      - applicationsettingsand databaseschema controller/  - pagelogicorganizedbyHTTPmethods (GET, POST) model/        - databasequeries route/        - routeinformationand middleware shared/      - packagesfor templates, MySQL, cryptography, sessions, and json static/      - locationofstaticallyservedfileslikeCSSand JS template/    - HTMLtemplates gowebapp.db  - SQLitedatabase gowebapp.go  - applicationentrypoint 

第三方包

github.com/gorilla/context              - registry for global request variables github.com/gorilla/sessions             - cookie and filesystem sessions github.com/go-sql-driver/mysql          - MySQL driver github.com/haisum/recaptcha             - Google reCAPTCHA support github.com/jmoiron/sqlx                 - MySQL general purpose extensions github.com/josephspurrier/csrfbanana    - CSRF protection for gorilla sessions github.com/julienschmidt/httprouter     - high performance HTTP request router github.com/justinas/alice               - middleware chaining github.com/mattn/go-sqlite3             - SQLite driver golang.org/x/crypto/bcrypt              - password hashing algorithm
github.com/gorilla/context              - registryfor global requestvariables github.com/gorilla/sessions            - cookieand filesystemsessions github.com/go-sql-driver/mysql          - MySQLdriver github.com/haisum/recaptcha            - GooglereCAPTCHAsupport github.com/jmoiron/sqlx                - MySQLgeneralpurposeextensions github.com/josephspurrier/csrfbanana    - CSRFprotectionfor gorillasessions github.com/julienschmidt/httprouter    - highperformanceHTTPrequestrouter github.com/justinas/alice              - middlewarechaining github.com/mattn/go-sqlite3            - SQLitedriver golang.org/x/crypto/bcrypt              - passwordhashingalgorithm 

程序入口

我希望我的main package, gowebapp.go ,只做下面几件事情:

  • 设置程序结构
  • 读取Json配置文件并传递给需要配置的包
  • 启动HTTP listener

通过使用这种策略,配置在一个地方可以使你应用程序很容易的添加或者删除某个组件。无论是标准库还是第三方包。

package main  import (  "encoding/json"  "log"  "os"  "runtime"   "github.com/josephspurrier/gowebapp/route"  "github.com/josephspurrier/gowebapp/shared/database"  "github.com/josephspurrier/gowebapp/shared/email"  "github.com/josephspurrier/gowebapp/shared/jsonconfig"  "github.com/josephspurrier/gowebapp/shared/recaptcha"  "github.com/josephspurrier/gowebapp/shared/server"  "github.com/josephspurrier/gowebapp/shared/session"  "github.com/josephspurrier/gowebapp/shared/view"  "github.com/josephspurrier/gowebapp/shared/view/plugin" )
package main   import (  "encoding/json"  "log"  "os"  "runtime"    "github.com/josephspurrier/gowebapp/route"  "github.com/josephspurrier/gowebapp/shared/database"  "github.com/josephspurrier/gowebapp/shared/email"  "github.com/josephspurrier/gowebapp/shared/jsonconfig"  "github.com/josephspurrier/gowebapp/shared/recaptcha"  "github.com/josephspurrier/gowebapp/shared/server"  "github.com/josephspurrier/gowebapp/shared/session"  "github.com/josephspurrier/gowebapp/shared/view"  "github.com/josephspurrier/gowebapp/shared/view/plugin" ) 

程序的配置定义在 configuration 中和保存在config变量中。

// config the settings variable var config = &configuration{}  // configuration contains the application settings type configuration struct {  Database  database.Databases      `json:"Database"`  Email     email.SMTPInfo          `json:"Email"`  Recaptcha recaptcha.RecaptchaInfo `json:"Recaptcha"`  Server    server.Server           `json:"Server"`  Session   session.Session         `json:"Session"`  Template  view.Template           `json:"Template"`  View      view.View               `json:"View"` }  // ParseJSON unmarshals bytes to structs func (c *configuration) ParseJSON(b []byte) error {  return json.Unmarshal(b, &c) }
// config the settings variable var config = &configuration{}   // configuration contains the application settings type configuration struct {  Database  database.Databases      `json:"Database"`  Email    email.SMTPInfo          `json:"Email"`  Recaptcharecaptcha.RecaptchaInfo `json:"Recaptcha"`  Server    server.Server          `json:"Server"`  Session  session.Session        `json:"Session"`  Template  view.Template          `json:"Template"`  View      view.View              `json:"View"` }   // ParseJSON unmarshals bytes to structs func (c *configuration) ParseJSON(b []byte) error {  return json.Unmarshal(b, &c) } 

runtime 设置和flags 我们定义在init() 函数中,组建通过main()函数读取config.json里面的参数进行设置。

func init() {  // Verbose logging with file name and line number  log.SetFlags(log.Lshortfile)   // Use all CPU cores  runtime.GOMAXPROCS(runtime.NumCPU()) }  func main() {  // Load the configuration file  jsonconfig.Load("config"+string(os.PathSeparator)+"config.json", config)   // Configure the session cookie store  session.Configure(config.Session)   // Connect to database  database.Connect(config.Database)   // Configure the Google reCAPTCHA prior to loading view plugins  recaptcha.Configure(config.Recaptcha)   // Setup the views  view.Configure(config.View)  view.LoadTemplates(config.Template.Root, config.Template.Children)  view.LoadPlugins(plugin.TemplateFuncMap(config.View))   // Start the listener  server.Run(route.LoadHTTP(), route.LoadHTTPS(), config.Server) }
funcinit() {  // Verbose logging with file name and line number  log.SetFlags(log.Lshortfile)    // Use all CPU cores  runtime.GOMAXPROCS(runtime.NumCPU()) }   funcmain() {  // Load the configuration file  jsonconfig.Load("config"+string(os.PathSeparator)+"config.json", config)    // Configure the session cookie store  session.Configure(config.Session)    // Connect to database  database.Connect(config.Database)    // Configure the Google reCAPTCHA prior to loading view plugins  recaptcha.Configure(config.Recaptcha)    // Setup the views  view.Configure(config.View)  view.LoadTemplates(config.Template.Root, config.Template.Children)  view.LoadPlugins(plugin.TemplateFuncMap(config.View))    // Start the listener  server.Run(route.LoadHTTP(), route.LoadHTTPS(), config.Server) } 

共享包

我希望我的是低耦合的,每个组建都能定义自己的结构和配置。我不想注册一个全局的容器,因为那样会创建太多的依赖。我这么设计为了当程序启动的时候,一个Json配置文件通过解析然后通过 Configure() 或者  Load() 函数初始化每一个packages。许多共享 packages就像第三方包一样。

这种结构的好处是:

  • Each package in the shared/ folder only imports packages from the standard library or an external package so it’s easy to reuse the package in other applications
  • When adding configurable settings to each package, they only need to be added in two places: the config.json file and the package itself
  • If the API of an external package changes, it’s easy to update the wrapper without modifying any code in your core application

这个包只引用了标准库和一个第三方包:

package session  import (  "net/http"   "github.com/gorilla/sessions" )
package session   import (  "net/http"    "github.com/gorilla/sessions" ) 

这个包定义了一个叫Session struct,他的配置是从json文件读取的。一些变量只能在同一个包下访问,有些可以被外部包访问。

var (  // Store is the cookie store  Store *sessions.CookieStore  // Name is the session name  Name string )  // Session stores session level information type Session struct {  Options   sessions.Options `json:"Options"`   // Pulled from: http://www.gorillatoolkit.org/pkg/sessions#Options  Name      string           `json:"Name"`      // Name for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.Get  SecretKey string           `json:"SecretKey"` // Key for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.New }
var (  // Store is the cookie store  Store *sessions.CookieStore  // Name is the session name  Namestring )   // Session stores session level information type Session struct {  Options  sessions.Options `json:"Options"`  // Pulled from: http://www.gorillatoolkit.org/pkg/sessions#Options  Name      string          `json:"Name"`      // Name for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.Get  SecretKeystring          `json:"SecretKey"` // Key for: http://www.gorillatoolkit.org/pkg/sessions#CookieStore.New } 

Configure()函数传递结构代替各个参数,不需要在外部包修改代码(Json 文件除外)当Session结构体发生变化时。

// Configure the session cookie store func Configure(s Session) {  Store = sessions.NewCookieStore([]byte(s.SecretKey))  Store.Options = &s.Options  Name = s.Name }
// Configure the session cookie store funcConfigure(s Session) {  Store = sessions.NewCookieStore([]byte(s.SecretKey))  Store.Options = &s.Options  Name = s.Name } 

这个包用来调用 Instance()函数,这样核心程序就不需要直接引用 gorilla/sessions 包。

// Session returns a new session, never returns an error func Instance(r *http.Request) *sessions.Session {  session, _ := Store.Get(r, Name)  return session }
// Session returns a new session, never returns an error funcInstance(r *http.Request) *sessions.Session {  session, _ := Store.Get(r, Name)  return session } 

Models

所有的 models都应该保存在一个 model文件夹下。一般程序都会支持Mysql或者SQLite,但是可以很容易地改变成使用另一类型的数据库

// User table contains the information for each user type User struct {  Id         uint32    `db:"id"`  First_name string    `db:"first_name"`  Last_name  string    `db:"last_name"`  Email      string    `db:"email"`  Password   string    `db:"password"`  Status_id  uint8     `db:"status_id"`  Created_at time.Time `db:"created_at"`  Updated_at time.Time `db:"updated_at"`  Deleted    uint8     `db:"deleted"` }  // User_status table contains every possible user status (active/inactive) type User_status struct {  Id         uint8     `db:"id"`  Status     string    `db:"status"`  Created_at time.Time `db:"created_at"`  Updated_at time.Time `db:"updated_at"`  Deleted    uint8     `db:"deleted"` }
// User table contains the information for each user type User struct {  Id        uint32    `db:"id"`  First_namestring    `db:"first_name"`  Last_name  string    `db:"last_name"`  Email      string    `db:"email"`  Password  string    `db:"password"`  Status_id  uint8    `db:"status_id"`  Created_attime.Time `db:"created_at"`  Updated_attime.Time `db:"updated_at"`  Deleted    uint8    `db:"deleted"` }   // User_status table contains every possible user status (active/inactive) type User_status struct {  Id        uint8    `db:"id"`  Status    string    `db:"status"`  Created_attime.Time `db:"created_at"`  Updated_attime.Time `db:"updated_at"`  Deleted    uint8    `db:"deleted"` } 

函数的命名最好能够清晰明了,一看就能知道这个程序是做什么的。

// UserByEmail gets user information from email func UserByEmail(email string) (User, error) {  result := User{}  err := database.DB.Get(&result, `SELECT id, password, status_id, first_name FROM user WHERE email = ? LIMIT 1`, email)  return result, err }  // UserIdByEmail gets user id from email func UserIdByEmail(email string) (User, error) {  result := User{}  err := database.DB.Get(&result, "SELECT id FROM user WHERE email = ? LIMIT 1", email)  return result, err }  // UserCreate creates user func UserCreate(first_name, last_name, email, password string) error {  _, err := database.DB.Exec(`INSERT INTO user (first_name, last_name, email, password) VALUES (?,?,?,?)`, first_name, last_name, email, password)  return err }
// UserByEmail gets user information from email funcUserByEmail(emailstring) (User, error) {  result := User{}  err := database.DB.Get(&result, `SELECTid, password, status_id, first_nameFROMuserWHEREemail = ? LIMIT 1`, email)  return result, err }   // UserIdByEmail gets user id from email funcUserIdByEmail(emailstring) (User, error) {  result := User{}  err := database.DB.Get(&result, "SELECT id FROM user WHERE email = ? LIMIT 1", email)  return result, err }   // UserCreate creates user funcUserCreate(first_name, last_name, email, passwordstring) error {  _, err := database.DB.Exec(`INSERTINTOuser (first_name, last_name, email, password) VALUES (?,?,?,?)`, first_name, last_name, email, password)  return err } 

Routes

每个routes都定义在 route.go ,我决定使用 julienschmidt/httprouter来提高速度, justinas/alice用来实现chaining access control lists (ACLs)去控制主要的逻辑控制。所有的中间件都定义在一个地方。

我这里就不讲述中间件和路由整合到http或者https大家可以看我之前写的关于 Go语言的Http 中间件实现 这是我之前翻译的(译者)

// Load the routes and middleware func Load() http.Handler {  return middleware(routes()) }  // Load the HTTP routes and middleware func LoadHTTPS() http.Handler {  return middleware(routes()) }  // Load the HTTPS routes and middleware func LoadHTTP() http.Handler {  return middleware(routes())   // Uncomment this and comment out the line above to always redirect to HTTPS  //return http.HandlerFunc(redirectToHTTPS) }  // Optional method to make it easy to redirect from HTTP to HTTPS func redirectToHTTPS(w http.ResponseWriter, req *http.Request) {  http.Redirect(w, req, "https://"+req.Host, http.StatusMovedPermanently) }
// Load the routes and middleware funcLoad() http.Handler {  return middleware(routes()) }   // Load the HTTP routes and middleware funcLoadHTTPS() http.Handler {  return middleware(routes()) }   // Load the HTTPS routes and middleware funcLoadHTTP() http.Handler {  return middleware(routes())    // Uncomment this and comment out the line above to always redirect to HTTPS  //return http.HandlerFunc(redirectToHTTPS) }   // Optional method to make it easy to redirect from HTTP to HTTPS funcredirectToHTTPS(w http.ResponseWriter, req *http.Request) {  http.Redirect(w, req, "https://"+req.Host, http.StatusMovedPermanently) } 

这里我给大家展示几个路由的使用:

func routes() *httprouter.Router {  r := httprouter.New()   // Set 404 handler  r.NotFound = alice.   New().   ThenFunc(controller.Error404)   // Serve static files, no directory browsing  r.GET("/static/*filepath", hr.Handler(alice.   New().   ThenFunc(controller.Static)))   // Home page  r.GET("/", hr.Handler(alice.   New().   ThenFunc(controller.Index)))   // Login  r.GET("/login", hr.Handler(alice.   New(acl.DisallowAuth).   ThenFunc(controller.LoginGET)))  r.POST("/login", hr.Handler(alice.   New(acl.DisallowAuth).   ThenFunc(controller.LoginPOST)))  r.GET("/logout", hr.Handler(alice.   New().   ThenFunc(controller.Logout))) ... }
funcroutes() *httprouter.Router {  r := httprouter.New()    // Set 404 handler  r.NotFound = alice.  New().  ThenFunc(controller.Error404)    // Serve static files, no directory browsing  r.GET("/static/*filepath", hr.Handler(alice.  New().  ThenFunc(controller.Static)))    // Home page  r.GET("/", hr.Handler(alice.  New().  ThenFunc(controller.Index)))    // Login  r.GET("/login", hr.Handler(alice.  New(acl.DisallowAuth).  ThenFunc(controller.LoginGET)))  r.POST("/login", hr.Handler(alice.  New(acl.DisallowAuth).  ThenFunc(controller.LoginPOST)))  r.GET("/logout", hr.Handler(alice.  New().  ThenFunc(controller.Logout))) ... } 

中间件加入到handler中:

func middleware(h http.Handler) http.Handler {  // Prevents CSRF and Double Submits  cs := csrfbanana.New(h, session.Store, session.Name)  cs.FailureHandler(http.HandlerFunc(controller.InvalidToken))  cs.ClearAfterUsage(true)  cs.ExcludeRegexPaths([]string{"/static(.*)"})  csrfbanana.TokenLength = 32  csrfbanana.TokenName = "token"  csrfbanana.SingleToken = false  h = cs   // Log every request  h = logrequest.Handler(h)   // Clear handler for Gorilla Context  h = context.ClearHandler(h)   return h }
funcmiddleware(h http.Handler) http.Handler {  // Prevents CSRF and Double Submits  cs := csrfbanana.New(h, session.Store, session.Name)  cs.FailureHandler(http.HandlerFunc(controller.InvalidToken))  cs.ClearAfterUsage(true)  cs.ExcludeRegexPaths([]string{"/static(.*)"})  csrfbanana.TokenLength = 32  csrfbanana.TokenName = "token"  csrfbanana.SingleToken = false  h = cs    // Log every request  h = logrequest.Handler(h)    // Clear handler for Gorilla Context  h = context.ClearHandler(h)    return h } 

英文原文链接: http://www.josephspurrier.com/go-web-app-example/

Go Web App Example – Entry Point, File Structure, Models, and Routes

正文到此结束
Loading...