├── .gitignore ├── logo.png ├── 2-coupled-packages ├── definition │ ├── config.go │ └── database.go ├── database │ ├── user.go │ └── config.go ├── handler │ └── user_permissions_by_id.go ├── config │ └── permissions.go └── main.go ├── 3-independent-packages ├── database │ ├── user.go │ └── config.go ├── main.go ├── handler │ └── user_permissions_by_id.go └── config │ └── permissions.go ├── README.md └── 1-single-package ├── handler.go ├── database.go └── main.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PerimeterX/ok-lets-go/HEAD/logo.png -------------------------------------------------------------------------------- /2-coupled-packages/definition/config.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | var RolePermissions map[string][]string 4 | -------------------------------------------------------------------------------- /2-coupled-packages/database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type SomeUserDB struct{} 4 | 5 | func (db *SomeUserDB) UserRoleByID(id string) string { 6 | // implementation 7 | } 8 | -------------------------------------------------------------------------------- /3-independent-packages/database/user.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type SomeUserDB struct{} 4 | 5 | func (db *SomeUserDB) UserRoleByID(id string) string { 6 | // implementation 7 | } 8 | -------------------------------------------------------------------------------- /2-coupled-packages/database/config.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type SomeConfigDB struct{} 4 | 5 | func (db *SomeConfigDB) AllPermissions() map[string][]string { 6 | // implementation 7 | } 8 | -------------------------------------------------------------------------------- /3-independent-packages/database/config.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | type SomeConfigDB struct{} 4 | 5 | func (db *SomeConfigDB) AllPermissions() map[string][]string { 6 | // implementation 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OK Let’s Go: Three Approaches to Structuring Go Code 2 | 3 | ![ok-lets-go](logo.png) 4 | 5 | This repository contains the structures of the approaches detailed in [the article](https://www.perimeterx.com/blog/ok-lets-go/). 6 | If you found any issues in this repository, feel free to open an issue or comment in the article page. 7 | -------------------------------------------------------------------------------- /1-single-package/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | func UserPermissionsByID(w http.ResponseWriter, r *http.Request) { 10 | id := r.URL.Query()["id"][0] 11 | role := userDBInstance.userRoleByID(id) 12 | permissions := rolePermissions[role] 13 | fmt.Fprint(w, strings.Join(permissions, ", ")) 14 | } 15 | -------------------------------------------------------------------------------- /2-coupled-packages/handler/user_permissions_by_id.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/definition" 6 | "net/http" 7 | "strings" 8 | ) 9 | 10 | func UserPermissionsByID(w http.ResponseWriter, r *http.Request) { 11 | id := r.URL.Query()["id"][0] 12 | role := definition.UserDBInstance.UserRoleByID(id) 13 | permissions := definition.RolePermissions[role] 14 | fmt.Fprint(w, strings.Join(permissions, ", ")) 15 | } 16 | -------------------------------------------------------------------------------- /2-coupled-packages/config/permissions.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/definition" 5 | "time" 6 | ) 7 | 8 | // Since the definition package must not contain any logic, 9 | // managing configuration is implemented in a config package. 10 | func InitPermissions() { 11 | definition.RolePermissions = definition.ConfigDBInstance.AllPermissions() 12 | go func() { 13 | for { 14 | time.Sleep(time.Hour) 15 | definition.RolePermissions = definition.ConfigDBInstance.AllPermissions() 16 | } 17 | }() 18 | } 19 | -------------------------------------------------------------------------------- /2-coupled-packages/definition/database.go: -------------------------------------------------------------------------------- 1 | package definition 2 | 3 | // Note that in this approach both the singleton instance 4 | // and its interface type are declared in the definition 5 | // package. Make sure this package does not contain any 6 | // logic, otherwise it might need to import other packages 7 | // and its neutral nature is compromised. 8 | var ( 9 | UserDBInstance UserDB 10 | ConfigDBInstance ConfigDB 11 | ) 12 | 13 | type UserDB interface { 14 | UserRoleByID(id string) string 15 | } 16 | 17 | type ConfigDB interface { 18 | AllPermissions() map[string][]string // maps from role to its permissions 19 | } 20 | -------------------------------------------------------------------------------- /3-independent-packages/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Note how the main package is the only one importing 4 | // other local packages. 5 | import ( 6 | "github.com/perimeterx/ok-lets-go/3-independent-packages/config" 7 | "github.com/perimeterx/ok-lets-go/3-independent-packages/database" 8 | "github.com/perimeterx/ok-lets-go/3-independent-packages/handler" 9 | "net/http" 10 | ) 11 | 12 | func main() { 13 | userDB := &database.SomeUserDB{} 14 | configDB := &database.SomeConfigDB{} 15 | permissionStorage := config.NewPermissionStorage(configDB) 16 | h := &handler.UserPermissionsByID{UserDB: userDB, PermissionsStorage: permissionStorage} 17 | http.Handle("/", h) 18 | http.ListenAndServe(":8080", nil) 19 | } 20 | -------------------------------------------------------------------------------- /2-coupled-packages/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Note how the main package is the only one importing 4 | // packages other than the definition package. 5 | import ( 6 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/config" 7 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/database" 8 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/definition" 9 | "github.com/perimeterx/ok-lets-go/2-coupled-packages/handler" 10 | "net/http" 11 | ) 12 | 13 | func main() { 14 | // This approach also uses singleton instances, and 15 | // again it's the initiator's responsibility to make 16 | // sure they're initialized. 17 | definition.UserDBInstance = &database.SomeUserDB{} 18 | definition.ConfigDBInstance = &database.SomeConfigDB{} 19 | config.InitPermissions() 20 | http.HandleFunc("/", handler.UserPermissionsByID) 21 | http.ListenAndServe(":8080", nil) 22 | } 23 | -------------------------------------------------------------------------------- /1-single-package/database.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // We use interfaces as the types of our database instances 4 | // to make it possible to write tests and use mock implementations. 5 | type userDB interface { 6 | userRoleByID(id string) string 7 | } 8 | 9 | // Note the naming `someConfigDB`. In actual cases we use 10 | // some DB implementation and name our structs accordingly. 11 | // For example, if we use MongoDB, we name our concrete 12 | // struct `mongoConfigDB`. If used in test cases, 13 | // a `mockConfigDB` can be declared, too. 14 | type someUserDB struct{} 15 | 16 | func (db *someUserDB) userRoleByID(id string) string { 17 | // Omitting the implementation details for clarity... 18 | } 19 | 20 | type configDB interface { 21 | allPermissions() map[string][]string // maps from role to its permissions 22 | } 23 | 24 | type someConfigDB struct{} 25 | 26 | func (db *someConfigDB) allPermissions() map[string][]string { 27 | // implementation 28 | } 29 | -------------------------------------------------------------------------------- /3-independent-packages/handler/user_permissions_by_id.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | ) 8 | 9 | // Declaring our local needs from the user db instance, 10 | type UserDB interface { 11 | UserRoleByID(id string) string 12 | } 13 | 14 | // ... and our local needs from the in memory permission storage. 15 | type PermissionStorage interface { 16 | RolePermissions(role string) []string 17 | } 18 | 19 | // Lastly, our handler cannot be purely functional, 20 | // since it requires references to non singleton instances. 21 | type UserPermissionsByID struct { 22 | UserDB UserDB 23 | PermissionsStorage PermissionStorage 24 | } 25 | 26 | func (u *UserPermissionsByID) ServeHTTP(w http.ResponseWriter, r *http.Request) { 27 | id := r.URL.Query()["id"][0] 28 | role := u.UserDB.UserRoleByID(id) 29 | permissions := u.PermissionsStorage.RolePermissions(role) 30 | fmt.Fprint(w, strings.Join(permissions, ", ")) 31 | } 32 | -------------------------------------------------------------------------------- /3-independent-packages/config/permissions.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Here we declare an interface representing our local 8 | // needs from the configuration db, namely, 9 | // the `AllPermissions` method. 10 | type PermissionDB interface { 11 | AllPermissions() map[string][]string // maps from role to its permissions 12 | } 13 | 14 | // Then we export a service than will provide the 15 | // permissions from memory, to use it, another package 16 | // will have to declare a local interface. 17 | type PermissionStorage struct { 18 | permissions map[string][]string 19 | } 20 | 21 | func NewPermissionStorage(db PermissionDB) *PermissionStorage { 22 | s := &PermissionStorage{} 23 | s.permissions = db.AllPermissions() 24 | go func() { 25 | for { 26 | time.Sleep(time.Hour) 27 | s.permissions = db.AllPermissions() 28 | } 29 | }() 30 | return s 31 | } 32 | 33 | func (s *PermissionStorage) RolePermissions(role string) []string { 34 | return s.permissions[role] 35 | } 36 | -------------------------------------------------------------------------------- /1-single-package/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // As noted above, since we plan to only have one instance 9 | // for those 3 services, we'll declare a singleton instance, 10 | // and make sure we only use them to access those services. 11 | var ( 12 | userDBInstance userDB 13 | configDBInstance configDB 14 | rolePermissions map[string][]string 15 | ) 16 | 17 | func main() { 18 | // Our singleton instances will later be assumed 19 | // initialized, it is the initiator's responsibility 20 | // to initialize them. 21 | // The main function will do it with concrete 22 | // implementation, and test cases, if we plan to 23 | // have those, may use mock implementations instead. 24 | userDBInstance = &someUserDB{} 25 | configDBInstance = &someConfigDB{} 26 | initPermissions() 27 | http.HandleFunc("/", UserPermissionsByID) 28 | http.ListenAndServe(":8080", nil) 29 | } 30 | 31 | // This will keep our permissions up to date in memory. 32 | func initPermissions() { 33 | rolePermissions = configDBInstance.allPermissions() 34 | go func() { 35 | for { 36 | time.Sleep(time.Hour) 37 | rolePermissions = configDBInstance.allPermissions() 38 | } 39 | }() 40 | } 41 | --------------------------------------------------------------------------------