Last active
September 9, 2022 14:57
-
-
Save rem-lag/a7757fa0467014172561fe659d5282ed to your computer and use it in GitHub Desktop.
A simple Go / Golang HTTP server implemented using interfaces
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"errors" | |
"fmt" | |
"net/http" | |
) | |
/* | |
Example code demonstrating how to use interfaces in Go | |
Defines a simple server which logs some actions on the server and prints hello | |
to a user making a GET request to the /hello endpoint | |
The user id will be passed as a param in the url which is used to look up the user name in a map | |
Example uses interfaces to decouple specifically typed structs/functions from the code that actually implements them | |
Different structs/funcs can be created or brought in and as long as they meet the interface they can just be dropped | |
in to the existing code | |
usage: | |
go run interface_example.go | |
curl 0.0.0.0:8080/hello?user_id=123 -> "Hello, CoolCat" returned | |
Server logs will show: | |
In Controller SayHello | |
in SimpleLogic SayHello for 123 | |
User ID 123 | |
User CoolCat | |
example adapted from | |
Learning Go by Jon Bodner (O’Reilly). Copyright 2021 Jon Bodner, 978-1-492-07721-3. | |
*/ | |
// server side logging function | |
// Will meet Logger interface | |
func LogOutput(message string) { | |
fmt.Println(message) | |
} | |
// stores user id and name as map | |
// This will meet DataStore interface below | |
// As with every other definition that meets an interface, it doesn't know anything | |
// about the interface and that fact is never explicitly made | |
type SimpleDataStore struct { | |
userData map[string]string | |
} | |
// Get a user's name from their id using SimpleDataStore struct | |
// Method of SimpleDataStore, definition causes SDS to meet DataStore interface | |
func (sds SimpleDataStore) UserNameForID(userID string) (string, bool) { | |
fmt.Println("User ID", userID) | |
fmt.Println("User", sds.userData[userID]) | |
name, ok := sds.userData[userID] | |
return name, ok | |
} | |
// factory function that creates type SimpleDataStore from provided map | |
func NewSimpleDataStore(ud map[string]string) SimpleDataStore { | |
return SimpleDataStore{ | |
userData: ud, | |
} | |
} | |
// interface for decoupling following logic from SimpleDataStore | |
// is met by SimpleDataStore | |
type DataStore interface { | |
UserNameForID(userID string) (string, bool) | |
} | |
// Second interface decouples logging implemented in business logic from LogOutput | |
// LogOutput does not have Log method, this is handled by below definitions so it meets interface | |
type Logger interface { | |
Log(message string) | |
} | |
// LoggerAdapter type fits function signature of LogOutput | |
// LogOutput can later be made this type | |
type LoggerAdapter func(message string) | |
// LoggerAdapter method wich meets Logger interface | |
// a function that is LoggerAdapter type can use this method to call the originally definied function | |
// ie LogOutput -> LoggerAdapter type, calling logger.Log(message) ends up calling LogOutput(message) | |
func (lg LoggerAdapter) Log(message string) { | |
lg(message) | |
} | |
// SimpleLog struct stores the logger and datastore we need for our business logic | |
// It's methods will be the implementation of the business logic | |
// Since Logger and DataStore are interfaces, any struct or function who meet the interface | |
// can be stored here and the methods defined by the interface will be accessible to the logic | |
// the actual logic therefore does not depend on LogOutput or SimpleDataStore | |
// New/different loggers that also meet the interface could be created and substituted within main with no other changes | |
// SimpleLogic itself will meet the Logic interface | |
type SimpleLogic struct { | |
l Logger | |
ds DataStore | |
} | |
// SimpleLogic method | |
// Uses supplied Logger to log when it is evoked | |
// Uses supplied DataStore to get the user's name and print a welcome message for them | |
func (sl SimpleLogic) SayHello(userID string) (string, error) { | |
sl.l.Log("In SimpleLogic SayHello for " + userID) | |
name, ok := sl.ds.UserNameForID(userID) | |
if !ok { | |
return "", errors.New("unknown user") | |
} | |
return "Hello, " + name, nil | |
} | |
// Same as SayHello but says Goodbye instead | |
func (sl SimpleLogic) SayGoodbye(userID string) (string, error) { | |
sl.l.Log("in SayGoodbye for " + userID) | |
name, ok := sl.ds.UserNameForID(userID) | |
if !ok { | |
return "", errors.New("unknown user") | |
} | |
return "Goodbye, " + name, nil | |
} | |
// Factory function that creates an instance of the SimpleLogic struct | |
// using the datastore and logger we want to use | |
func NewSimpleLogic(l Logger, ds DataStore) SimpleLogic { | |
return SimpleLogic{ | |
l: l, | |
ds: ds, | |
} | |
} | |
// Logic interface that will be used by controller to say hello | |
// Is met by SimpleLogic struct | |
// SayGoodbye is not listed in this interface, it will therefore | |
// not be available to whatever uses the Logic interface | |
type Logic interface { | |
SayHello(userID string) (string, error) | |
} | |
// Controller struct also stores the Logger along with the Logic interface giving it access to | |
// SayHello to stored struct that meets the interface | |
// the SimpleLogic struct also contains a Logger and meets the Logic interface, however the Logic | |
// interface only provides access to the SimpleLogic's SayHello and not the logger in the SimpleLogic struct | |
type Controller struct { | |
l Logger | |
logic Logic | |
} | |
// Controller method which wraps the Logic interface's SayHello method | |
// This allows us to go from what we have (in this case an http request where the user id is a url param) | |
// and get it into the form we need (a user id) to pass to the SayHello | |
// method of the Logic interface (which is the SayHello method defined in the SimpleLogic Struct) | |
func (c Controller) SayHello(w http.ResponseWriter, r *http.Request) { | |
c.l.Log("In Controller SayHello") | |
userID := r.URL.Query().Get("user_id") | |
message, err := c.logic.SayHello(userID) | |
if err != nil { | |
w.WriteHeader(http.StatusBadRequest) | |
w.Write([]byte(err.Error())) | |
return | |
} | |
w.Write([]byte(message)) | |
} | |
// Factory function for creating a Controller instance | |
func NewController(l Logger, logic Logic) Controller { | |
return Controller{ | |
l: l, | |
logic: logic, | |
} | |
} | |
// Implement everything from above | |
// If one created a new implimentation of something that met any of the interfaces | |
// this would be the only place that needs to be changed | |
// For example, say you wanted a more elaborate hello greeting or used a different datastore | |
// you could define a new struct which implements a method called SayHello that takes a string | |
// and returns a string/error. Same idea for changing the datastore | |
// You'd then just have to change those things here but not touch anything | |
// within the logic of controller or whatever else is implementing the relevant interface | |
// Same thing if you wanted a different logger | |
func main() { | |
// Create SimpleDataStore instance | |
data := map[string]string{ | |
"123": "CoolCat", | |
"456": "Trogdor", | |
"789": "xXxLegolasxXx420", | |
} | |
ds := NewSimpleDataStore(data) | |
// Switch LogOutput to LoggerAdapter type | |
l := LoggerAdapter(LogOutput) | |
// SimpleDataStore and LoggerAdapter meet | |
// Logger and DataStore interfaces so can be used | |
// to instantiate SimpleLogic struct | |
logic := NewSimpleLogic(l, ds) | |
// SimpleLogic struct meets Logic interface | |
// and is used to instantiate our controller | |
c := NewController(l, logic) | |
// Start server and create /hello endpoint | |
// Which use's the controllers SayHello method when a GET | |
// request is made | |
http.HandleFunc("/hello", c.SayHello) | |
http.ListenAndServe(":8080", nil) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment