1 of 30

Idiomatic Design Patterns in Go

@ryan0x44

2 of 30

‘Talk less about frameworks,�more about design’

(Paraphrasing GolangUK 2016 keynote by Dave Cheney)

3 of 30

Books

4 of 30

“Idiomatic”

Doing it “the Go way” / best practices a la:

5 of 30

“Software Design Patterns”

“Reusable solution

to a commonly occurring problem

within a given context in software design”

  • Wikipedia

6 of 30

23 Classic Design Patterns

Creational - relates to instantiation

  • e.g. Factory pattern

Structural - relates to composition

  • e.g. Decorator pattern

Behavioral - relates to communication

  • e.g. Iterator pattern

7 of 30

Creational: Factory pattern

Initialize a value for an interface,�but let the Factory decide which implementation �to instantiate.

  • a.k.a. Factory Method pattern

8 of 30

// Creational: Factory pattern example

func NewWriter(kind string) (io.Writer, error) {

switch kind {

case "mywriter":

return MyWriter{}, nil

case "stderr":

return os.Stderr, nil

default:

return nil, fmt.Errorf("Kind invalid: %s", kind)

}

}

type MyWriter struct{}

func (w MyWriter) Write(p []byte) (n int, err error) {

fmt.Printf("%s", p)

return len(p), nil

}

9 of 30

// Creational: Factory pattern example (cont.)

package main

func main() {

// Get user preference for writer

kind := "mywriter"

if len(os.Args) > 1 && os.Args[1] == "stderr" {

kind = "stderr"

}

// Create writer and write some output

writer, _ := NewWriter(kind)

writer.Write([]byte("Hello world\n"))

}

10 of 30

Structural: Decorator pattern

Add behavior,

without modifying original function/method,

keeping same interface.

  • Useful for adhering to the �Single Responsibility Principle.

11 of 30

// Structural: Decorator pattern example

func main() {

http.HandleFunc("/", helloEndpoint)

_ = http.ListenAndServe(":8080", nil)

}

func helloEndpoint(w http.ResponseWriter, r *http.Request) {

fmt.Fprintf(w, "Hello world")

}

12 of 30

// Structural: Decorator pattern example (cont.)

func main() {

http.HandleFunc("/", helloEndpoint)

http.HandleFunc("/logged", logDecorator(helloEndpoint))

_ = http.ListenAndServe(":8080", nil)

}

func helloEndpoint(w http.ResponseWriter, r *http.Request) {

fmt.Fprintf(w, "Hello world")

}

func logDecorator(fn http.HandlerFunc) http.HandlerFunc {

return func(w http.ResponseWriter, r *http.Request) {

log.Printf("%s %s", r.Method, r.URL)

fn(w, r)

}

}

13 of 30

Behavioral: Iterator pattern

Accesses elements sequentially,

without exposing underlying representation.

  • Can be used with a Decorator!
  • io.Reader & sql/database.Row are examples

14 of 30

// Behavioral: Iterator pattern example

type Iterator struct {

tasks []string

position int

}

// ErrEOF signals a graceful end of iteration

var ErrEOF = errors.New("EOF")

// Next will return the next task in the slice, or ErrEOF

func (t *Iterator) Next() (int, string, error) {

t.position++

if t.position > len(t.tasks) {

return t.position, "", ErrEOF

}

return t.position, t.tasks[t.position-1], nil

}

15 of 30

// Behavioral: Iterator pattern (cont.)

func main() {

tasks := task.GetExamples()

for {

i, t, err := tasks.Next()

if err == task.ErrEOF {

log.Printf("Done")

break

}

if err != nil {

log.Fatalf("Unknown error: %s", err)

}

log.Printf("Task %d: %s\n", i, t)

}

}

func GetExamples() Iterator {

return Iterator{

tasks: []string{

"say hello",

"say goodbye",

},

}

}

16 of 30

Dependency Inversion Principle

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.

B. Abstractions should not depend on details. Details should depend on abstractions.

17 of 30

DIP: Naive Layering

Client Layer

package client

package service

Service Layer

18 of 30

DIP: Inverted Layers

Client Layer

package client

package service

Service Layer

<< interface >>Client Service Interface

19 of 30

Dependency Injection pattern

“Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.

20 of 30

Dependency Injection pattern

Three “styles” of DI:

  • Constructor
  • Setter
  • Interface

Let’s look at the first two.

21 of 30

// Creational: Dependency Injection (Constructor) pattern

func main() {

s := NewMyService(os.Stderr)

s.WriteHello("world")

}

type MyService struct {

writer io.Writer

}

func NewMyService(writer io.Writer) MyService {

return MyService{

writer: writer,

}

}

func (s *MyService) WriteHello(m string) {

fmt.Fprintf(s.writer, "Hello %s\n", m)

}

22 of 30

// Creational: Dependency Injection (Setter) pattern

func main() {

s := NewMyService()

s.SetWriter(os.Stderr)

s.WriteHello("world")

}

type MyService struct {

writer io.Writer

}

func NewMyService() MyService {

return MyService{}

}

func (s *MyService) SetWriter(w io.Writer) {

s.writer = w

}

// (Same WriteHello method as last slide)

23 of 30

DI - Constructor vs. Setter?

Constructor - enforce valid dependencies

Setter - allow creating/changing whenever

Which one? Generally - Constructor.

Can use both if required.

24 of 30

Domain-Driven Design

Premise:

  • projects should focus on domain
  • base design on model of the domain
  • initiating collaboration between technical and domain experts to address domain problems

25 of 30

Repository Pattern

“Methods for retrieving domain objects should delegate to a specialized Repository object such that alternative storage implementations may be easily interchanged”

Abstract persistence/retrieval from business logic.

26 of 30

Repository Layering

Package postgres

Client Layer

package main

package (domain)

Postgres Implementation

<< interface >>�(Domain) Repository

27 of 30

// Repository pattern example - repository interface

package project

type Project struct {

ID int64

Name string

}

type Repository interface {

FindAll() ([]Project, error)

Store(Project) (Project, error)

Delete(Project) error

}

// Repository pattern example - in-memory implementation

package inmem

type ProjectRepository struct {

projects map[int64]string

highestID int64

}

28 of 30

// Repository pattern example - in-memory implementation (cont.)

func (r *ProjectRepository) Store(p project.Project) (project.Project, error) {

if r.projects == nil {

r.projects = map[int64]string{}

}

if p.ID <= 0 {

r.highestID++

p.ID = r.highestID

} else {

if _, exists := r.projects[p.ID]; !exists {

return p, fmt.Errorf("Cannot update project: %d", p.ID)

}

}

r.projects[p.ID] = p.Name

return p, nil

}

29 of 30

// Repository pattern example - usage in main function

func main() {

projectsRepo := inmem.ProjectRepository{}

newProject, _ := projectsRepo.Store(

project.Project{

Name: "First project",

},

)

newProject.Name = "First project - updated!"

_, _ = projectsRepo.Store(newProject)

}

30 of 30

Thank you!

I hope this encourages you to read/learn more about design patterns,�+ talk more about the design of your code.�

Questions? @ryan0x44