有很多可以快速搭建Go web项目的开源框架,与其用一个开源框架,我更愿意自己Go的原生的东西去构建一个带认证功能的model-view-controller (MVC) web 程序。记住,这只是众多构建你web 项目方法的一种。
可以在Github查看项目的代码:https://github.com/josephspurrier/gowebapp
项目文件结构
1 2 3 4 5 6 7 8 9 |
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 |
第三方包
1 2 3 4 5 6 7 8 9 10 |
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 |
程序入口
我希望我的main package,gowebapp.go,只做下面几件事情:
- 设置程序结构
- 读取Json配置文件并传递给需要配置的包
- 启动HTTP listener
通过使用这种策略,配置在一个地方可以使你应用程序很容易的添加或者删除某个组件。无论是标准库还是第三方包。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
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变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 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) } |
runtime 设置和flags 我们定义在init() 函数中,组建通过main()函数读取config.json里面的参数进行设置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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) } |
共享包
我希望我的项目是低耦合的,每个组件都能定义自己的结构和配置。我不想注册一个全局的容器,因为那样会创建太多的依赖。我这么设计为了当程序启动的时候,一个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
这个包只引用了标准库和一个第三方包:
1 2 3 4 5 6 7 |
package session import ( "net/http" "github.com/gorilla/sessions" ) |
这个包定义了一个叫Session struct,他的配置是从json文件读取的。一些变量只能在同一个包下访问,有些可以被外部包访问。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 } |
Configure() 函数传递结构代替各个参数,不需要在外部包修改代码(Json 文件除外)当Session结构体发生变化时。
1 2 3 4 5 6 |
// Configure the session cookie store func Configure(s Session) { Store = sessions.NewCookieStore([]byte(s.SecretKey)) Store.Options = &s.Options Name = s.Name } |
这个包用来调用Instance()函数,这样核心程序就不需要直接引用gorilla/sessions 包。
1 2 3 4 5 |
// Session returns a new session, never returns an error func Instance(r *http.Request) *sessions.Session { session, _ := Store.Get(r, Name) return session } |
Models
所有的 models都应该保存在一个 model文件夹下。一般程序都会支持Mysql或者SQLite,但是可以很容易地改变成使用另一类型的数据库
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 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"` } |
函数的命名最好能够清晰明了,一看就能知道这个程序是做什么的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 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 } |
Routes
每个routes都定义在route.go,我决定使用julienschmidt/httprouter来提高速度,justinas/alice用来实现chaining access control lists (ACLs)去控制主要的逻辑控制。所有的中间件都定义在一个地方。
我这里就不讲述中间件和路由整合到http或者https大家可以看我之前写的关于Go语言的Http 中间件实现 这是我之前翻译的(译者)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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) } |
这里我给大家展示几个路由的使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
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))) ... } |
中间件加入到handler中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 } |
英文原文链接:http://www.josephspurrier.com/go-web-app-example/
有疑问加站长微信联系(非本文作者)