有很多可以快速搭建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 ,只做下面几件事情:
通过使用这种策略,配置在一个地方可以使你应用程序很容易的添加或者删除某个组件。无论是标准库还是第三方包。
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就像第三方包一样。
这种结构的好处是:
这个包只引用了标准库和一个第三方包:
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都应该保存在一个 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都定义在 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