Skip to content

Instantly share code, notes, and snippets.

@rem-lag
Last active September 9, 2022 14:57
Show Gist options
  • Save rem-lag/a7757fa0467014172561fe659d5282ed to your computer and use it in GitHub Desktop.
Save rem-lag/a7757fa0467014172561fe659d5282ed to your computer and use it in GitHub Desktop.
A simple Go / Golang HTTP server implemented using interfaces
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