├── README.md ├── basics ├── enums.go ├── funcitonaloptions │ ├── README.md │ └── main.go ├── phantomtypes │ ├── README.md │ └── main.go └── pipelinecmds │ ├── README.md │ └── main.go ├── behavioral ├── mediator │ ├── chat.go │ └── main.go ├── memento │ ├── history.go │ ├── main.go │ ├── memento.go │ └── tasks.go └── observer │ ├── main.go │ └── observer.go ├── concurrency ├── channels │ ├── channels.go │ └── channels_test.go ├── deadlock │ └── deadlock.go ├── fanin │ ├── README.md │ ├── fanin.go │ ├── main.go │ └── say.go └── tasks │ ├── README.md │ ├── slice.go │ ├── slice_test.go │ ├── tasks.go │ └── tasks_test.go ├── go.mod ├── go.sum ├── minimal-docker ├── Dockerfile.large ├── Dockerfile.small ├── Dockerfile.xsmall ├── go.mod └── main.go └── web ├── embeded ├── frontend │ ├── index.html │ └── static │ │ └── app.js └── main.go └── gracefulshutdown ├── README.md └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # Go Patterns 2 | 3 | This repo keeps different approaches and patterns for `Go` that I find helpful. 4 | 5 | ## Basic 6 | 7 | - [Functional Options](./basics/funcitonaloptions) (basics/funcitonaloptions) 8 | - [Pipeline Commands](./basics/pipelinecmds) (basics/pipelinecmds) 9 | - [Phantom Types](./basics/phantomtypes) (basics/phantomtypes) 10 | 11 | ## Concurrency 12 | 13 | - [Concurrent Tasks](./concurrency/tasks/) (concurrency/tasks) 14 | 15 | ## Web 16 | - [Gracefull Shutdown](./web/gracefulshutdown/) (web/gracefulshutdown) -------------------------------------------------------------------------------- /basics/enums.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type Season int64 6 | 7 | const ( 8 | Undefined Season = iota 9 | Spring 10 | Summer 11 | Autumn 12 | Winter 13 | ) 14 | 15 | func (s Season) String() string { 16 | switch s { 17 | case Undefined: 18 | return "" 19 | case Spring: 20 | return "spring" 21 | case Summer: 22 | return "summer" 23 | case Autumn: 24 | return "autumn" 25 | case Winter: 26 | return "winter" 27 | } 28 | return "unknown" 29 | } 30 | 31 | func main() { 32 | season1 := Spring 33 | season2 := Winter 34 | 35 | fmt.Printf( 36 | "%s == %s is %t\n", 37 | season1, season2, season1 == season2, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /basics/funcitonaloptions/README.md: -------------------------------------------------------------------------------- 1 | # Functional Options 2 | 3 | Functional Options is a pattern that feels very natural for Go. 4 | And it's helpful to create extensible configuration APIs for any case. 5 | 6 | - Let you write APIs that can grow with time. 7 | - Make the default options use case to be the simplest. 8 | - Provide meaningful configuration parameters. 9 | -------------------------------------------------------------------------------- /basics/funcitonaloptions/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Options struct { 8 | Host string 9 | Port int 10 | } 11 | 12 | type Service struct { 13 | options *Options 14 | } 15 | 16 | type Setter func(*Options) 17 | 18 | func WithHost(host string) Setter { 19 | return func(opts *Options) { 20 | opts.Host = host 21 | } 22 | } 23 | 24 | func WithPort(port int) Setter { 25 | return func(opts *Options) { 26 | opts.Port = port 27 | } 28 | } 29 | 30 | func New(setters ...Setter) *Service { 31 | options := &Options{ 32 | Host: "url", 33 | Port: 8080, 34 | } 35 | 36 | for _, set := range setters { 37 | set(options) 38 | } 39 | 40 | return &Service{ 41 | options: options, 42 | } 43 | } 44 | 45 | func main() { 46 | fmt.Println("Functional Patterns") 47 | service := New( 48 | WithHost("127.0.0.1"), 49 | WithPort(5000), 50 | ) 51 | fmt.Printf( 52 | "%s:%d\n", 53 | service.options.Host, 54 | service.options.Port, 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /basics/phantomtypes/README.md: -------------------------------------------------------------------------------- 1 | # Phantom Types 2 | 3 | __Phantom Type__ is a type that doesn’t use at least one of its generic type parameters. 4 | 5 | For example, 6 | 7 | ```go 8 | // Roles. 9 | type Basic struct{} 10 | type Admin struct{} 11 | 12 | // User. 13 | type User[T Basic | Admin] struct { 14 | name string 15 | } 16 | ``` 17 | 18 | And even we don't use the type parameter `T` in the `User` struct, 19 | it allows to check type rules more strictly. -------------------------------------------------------------------------------- /basics/phantomtypes/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Roles. 6 | type Basic struct{} 7 | type Admin struct{} 8 | 9 | // User. 10 | type User[T Basic | Admin] struct { 11 | name string 12 | } 13 | 14 | // createUser is only available for admins. 15 | func createUser[T User[Admin]](user T) { 16 | fmt.Printf("%s creates a user\n", user) 17 | } 18 | 19 | // viewUser is available for all users. 20 | func viewUser[T User[Basic] | User[Admin]](user T) { 21 | fmt.Printf("%s views a user\n", user) 22 | } 23 | 24 | func main() { 25 | basicUser := User[Basic]{"Bob"} 26 | adminUser := User[Admin]{"Jonh"} 27 | 28 | viewUser(basicUser) 29 | viewUser(adminUser) 30 | createUser(adminUser) 31 | } 32 | -------------------------------------------------------------------------------- /basics/pipelinecmds/README.md: -------------------------------------------------------------------------------- 1 | # Pipeline Commands 2 | 3 | You can make a pipeline of one `exec.Command` to another. 4 | 5 | ```sh 6 | $ grep -r err . | wc -l 7 | ``` -------------------------------------------------------------------------------- /basics/pipelinecmds/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os/exec" 7 | ) 8 | 9 | func main() { 10 | // Define `grep` command. 11 | grep := exec.Command("grep", "-r", "err", ".") 12 | out, err := grep.StdoutPipe() 13 | if err != nil { 14 | log.Fatal(err) 15 | } 16 | 17 | // Start grep. 18 | if err := grep.Start(); err != nil { 19 | log.Fatal(err) 20 | } 21 | 22 | // Define `wc` command. 23 | wc := exec.Command("wc", "-l") 24 | wc.Stdin = out 25 | 26 | // Read combined output. 27 | data, err := wc.CombinedOutput() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | 32 | fmt.Printf("%s", data) 33 | } 34 | -------------------------------------------------------------------------------- /behavioral/mediator/chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | type User struct { 6 | Name string 7 | Messages []string 8 | } 9 | 10 | // Mediator 11 | type Chat struct { 12 | Users map[string]*User 13 | } 14 | 15 | func NewChat() *Chat { 16 | return &Chat{make(map[string]*User)} 17 | } 18 | 19 | func (c *Chat) Add(user User) { 20 | c.Users[user.Name] = &user 21 | } 22 | 23 | func (c *Chat) Say(to User, msg string) error { 24 | user, ok := c.Users[to.Name] 25 | if !ok { 26 | return fmt.Errorf("%s not in the chat\n", to.Name) 27 | } 28 | user.Messages = append(user.Messages, msg) 29 | return nil 30 | } 31 | 32 | func (c *Chat) SayAll(msg string) { 33 | for _, user := range c.Users { 34 | user.Messages = append(user.Messages, msg) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /behavioral/mediator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func main() { 8 | fmt.Println("Mediator pattern.") 9 | 10 | John := User{Name: "John"} 11 | Bob := User{Name: "Bob"} 12 | Alice := User{Name: "Alice"} 13 | 14 | chat := NewChat() 15 | chat.Add(Bob) 16 | chat.Add(John) 17 | 18 | chat.Say(Bob, "Hello, Bob!") 19 | chat.Say(Alice, "Hello, Alice!") 20 | chat.SayAll("Hello, All!") 21 | } 22 | -------------------------------------------------------------------------------- /behavioral/memento/history.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type History struct { 4 | history []Memento 5 | } 6 | 7 | func NewHistory() *History { 8 | return &History{make([]Memento, 0)} 9 | } 10 | 11 | func (h *History) Save(m Memento) { 12 | h.history = append(h.history, m) 13 | } 14 | 15 | func (h *History) Undo() Memento { 16 | if len(h.history) > 1 { 17 | n := len(h.history) - 1 18 | h.history = h.history[:n] 19 | return h.history[len(h.history)-1] 20 | } else { 21 | return Memento{} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /behavioral/memento/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | history := NewHistory() 7 | 8 | tasks := NewTasks() 9 | 10 | tasks.Add("Task 1") 11 | history.Save(tasks.Memento()) 12 | 13 | tasks.Add("Task 2") 14 | history.Save(tasks.Memento()) 15 | 16 | fmt.Println(tasks.All()) 17 | 18 | tasks.Restore(history.Undo()) 19 | fmt.Println(tasks.All()) 20 | 21 | tasks.Restore(history.Undo()) 22 | fmt.Println(tasks.All()) 23 | } 24 | -------------------------------------------------------------------------------- /behavioral/memento/memento.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Memento struct { 4 | list List 5 | } 6 | 7 | func (m *Memento) List() List { 8 | return m.list 9 | } 10 | -------------------------------------------------------------------------------- /behavioral/memento/tasks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type List []string 4 | 5 | type Tasks struct { 6 | list List 7 | } 8 | 9 | func NewTasks() *Tasks { 10 | return &Tasks{} 11 | } 12 | 13 | func (t *Tasks) Memento() Memento { 14 | return Memento{list: t.list} 15 | } 16 | 17 | func (t *Tasks) Restore(m Memento) { 18 | t.list = m.list 19 | } 20 | 21 | func (t *Tasks) Add(s string) { 22 | t.list = append(t.list, s) 23 | } 24 | 25 | func (t *Tasks) All() List { 26 | return t.list 27 | } 28 | -------------------------------------------------------------------------------- /behavioral/observer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type observer struct { 8 | id int 9 | } 10 | 11 | func (o *observer) OnNotify(e Event) { 12 | fmt.Printf( 13 | "observer %d recieved event %d\n", 14 | o.id, e.Data, 15 | ) 16 | } 17 | 18 | type notifier struct { 19 | observers map[Observer]struct{} 20 | } 21 | 22 | func (n *notifier) Register(o Observer) { 23 | n.observers[o] = struct{}{} 24 | } 25 | 26 | func (n *notifier) Unregister(o Observer) { 27 | delete(n.observers, o) 28 | } 29 | 30 | func (n *notifier) Notify(e Event) { 31 | for o := range n.observers { 32 | o.OnNotify(e) 33 | } 34 | } 35 | 36 | func main() { 37 | n := notifier{ 38 | observers: map[Observer]struct{}{}, 39 | } 40 | 41 | n.Register(&observer{1}) 42 | n.Register(&observer{2}) 43 | 44 | n.Notify(Event{1}) 45 | n.Notify(Event{101}) 46 | n.Notify(Event{9999}) 47 | } 48 | -------------------------------------------------------------------------------- /behavioral/observer/observer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Event defines an indication of a some occurence 4 | type Event struct { 5 | // Data in this case is a simple int. 6 | Data int 7 | } 8 | 9 | // Observer defines a standard interface 10 | // to listen for a specific event. 11 | type Observer interface { 12 | // OnNotify allows to publsh an event 13 | OnNotify(Event) 14 | } 15 | 16 | // Notifier is the instance being observed. 17 | type Notifier interface { 18 | // Register itself to listen/observe events. 19 | Register(Observer) 20 | // Remove itself from the collection of observers/listeners. 21 | Unregister(Observer) 22 | // Notify publishes new events to listeners. 23 | Notify(Event) 24 | } 25 | -------------------------------------------------------------------------------- /concurrency/channels/channels.go: -------------------------------------------------------------------------------- 1 | package main 2 | -------------------------------------------------------------------------------- /concurrency/channels/channels_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "testing" 4 | 5 | func BenchmarkUnbufferedChannelInt(b *testing.B) { 6 | ch := make(chan int) 7 | go func() { 8 | for { 9 | <-ch 10 | } 11 | }() 12 | for i := 0; i < b.N; i++ { 13 | ch <- i 14 | } 15 | } 16 | 17 | func BenchmarkBufferedChannelInt(b *testing.B) { 18 | ch := make(chan int, 1) 19 | go func() { 20 | for { 21 | <-ch 22 | } 23 | }() 24 | for i := 0; i < b.N; i++ { 25 | ch <- i 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /concurrency/deadlock/deadlock.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | var ( 9 | lock1, lock2 sync.Mutex 10 | ) 11 | 12 | func func1() { 13 | for { 14 | lock1.Lock() 15 | lock2.Lock() 16 | lock1.Unlock() 17 | lock2.Unlock() 18 | } 19 | } 20 | 21 | func func2() { 22 | for { 23 | // NOTICE: another order for locks 24 | lock2.Lock() 25 | lock1.Lock() 26 | lock1.Unlock() 27 | lock2.Unlock() 28 | } 29 | } 30 | 31 | func main() { 32 | ch := make(chan int) 33 | 34 | go func() { 35 | ch <- 1 36 | }() 37 | go func() { 38 | ch <- 2 39 | }() 40 | 41 | fmt.Println(<-ch) 42 | fmt.Println(<-ch) 43 | fmt.Println(<-ch) 44 | } 45 | -------------------------------------------------------------------------------- /concurrency/fanin/README.md: -------------------------------------------------------------------------------- 1 | # Fan-In Pattern 2 | 3 | __Fan-in__ is a concurrency pattern that allows merging input of several goroutines into one. 4 | 5 | For example, we have two __goroutines__ Alice and Bob. 6 | 7 | Each goroutine sends messages and we want to read it in one function. 8 | 9 | Let's implement it in 2 steps. -------------------------------------------------------------------------------- /concurrency/fanin/fanin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func FanIn(msgs ...<-chan string) chan string { 4 | out := make(chan string) 5 | 6 | for _, each := range msgs { 7 | 8 | go func(input <-chan string) { 9 | for { 10 | // read value from input 11 | val, ok := <-input 12 | // break if channel is closed 13 | if !ok { 14 | break 15 | } 16 | out <- val 17 | } 18 | }(each) 19 | 20 | } 21 | return out 22 | } 23 | -------------------------------------------------------------------------------- /concurrency/fanin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func main() { 6 | Alice := Say( 7 | "Alice", "hi", "how'r you doing?", "i'm good", 8 | ) 9 | Bob := Say( 10 | "Bob", "hey", "what's up", "great!", 11 | ) 12 | 13 | all := FanIn(Alice, Bob) 14 | 15 | for i := 0; i < 6; i++ { 16 | fmt.Println(<-all) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /concurrency/fanin/say.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | func Say(who string, msgs ...string) <-chan string { 6 | c := make(chan string) 7 | go func() { 8 | for _, msg := range msgs { 9 | c <- fmt.Sprintf("%s said, %s", who, msg) 10 | } 11 | close(c) 12 | }() 13 | return c 14 | } 15 | -------------------------------------------------------------------------------- /concurrency/tasks/README.md: -------------------------------------------------------------------------------- 1 | # Concurrent Tasks 2 | 3 | This package provides a set of utilities for managing concurrent tasks in Go. 4 | -------------------------------------------------------------------------------- /concurrency/tasks/slice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | // Slice is a thread-safe slice. 8 | type Slice[T any] struct { 9 | sync.Mutex 10 | slice []T 11 | } 12 | 13 | // NewSlice creates a new thread-safe slice. 14 | func NewSlice[T any](size int) *Slice[T] { 15 | return &Slice[T]{slice: make([]T, 0, size)} 16 | } 17 | 18 | // Append method appends an item to the slice safely. 19 | func (s *Slice[T]) Append(value T) { 20 | s.Lock() 21 | s.slice = append(s.slice, value) 22 | s.Unlock() 23 | } 24 | 25 | // Get method returns the item at the specified index. 26 | func (s *Slice[T]) Get(index int) T { 27 | s.Lock() 28 | defer s.Unlock() 29 | return s.slice[index] 30 | } 31 | 32 | // Len method returns the length of the Slice. 33 | func (s *Slice[T]) Len() int { 34 | s.Lock() 35 | defer s.Unlock() 36 | return len(s.slice) 37 | } 38 | 39 | // Slice method returns the underlying slice. 40 | func (s *Slice[T]) Slice() []T { 41 | s.Lock() 42 | defer s.Unlock() 43 | return s.slice 44 | } 45 | -------------------------------------------------------------------------------- /concurrency/tasks/slice_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAppend(t *testing.T) { 11 | // Create a new Slice 12 | s := NewSlice[int](0) 13 | 14 | // Append a value to the Slice 15 | s.Append(10) 16 | 17 | // Verify that the value was appended correctly 18 | assert.Len(t, s.slice, 1) 19 | assert.Equal(t, 10, s.slice[0]) 20 | } 21 | 22 | func TestAppendConcurrent(t *testing.T) { 23 | // Number of goroutines to spawn 24 | numGoroutines := 100 25 | 26 | // Create a new Slice 27 | s := NewSlice[int](numGoroutines) 28 | 29 | // Create a WaitGroup to synchronize goroutines 30 | var wg sync.WaitGroup 31 | wg.Add(numGoroutines) 32 | 33 | // Spawn multiple goroutines to concurrently append values to the Slice 34 | for i := 0; i < numGoroutines; i++ { 35 | go func(value int) { 36 | defer wg.Done() 37 | s.Append(value) 38 | }(i) 39 | } 40 | 41 | // Wait for all goroutines to finish 42 | wg.Wait() 43 | 44 | // Verify that all values were appended correctly 45 | expectedSlice := make([]int, numGoroutines) 46 | for i := 0; i < numGoroutines; i++ { 47 | expectedSlice[i] = i 48 | } 49 | assert.Len(t, s.slice, numGoroutines) 50 | assert.ElementsMatch(t, expectedSlice, s.slice) 51 | } 52 | -------------------------------------------------------------------------------- /concurrency/tasks/tasks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | ) 8 | 9 | // Task represents an interface for executing tasks. 10 | type Task interface { 11 | Execute(ctx context.Context) error 12 | } 13 | 14 | // TaskFunc represents a function that can be executed as a task. 15 | type TaskFunc func(ctx context.Context) error 16 | 17 | func (tf TaskFunc) Execute(ctx context.Context) error { 18 | return tf(ctx) 19 | } 20 | 21 | // ExecuteTasks executes a list of tasks concurrently. 22 | // It takes a context, a list of tasks, and the number of workers to execute the tasks. 23 | // It cancels remaining tasks if any of the tasks fail and returns an error. 24 | func ExecuteTasks[T Task](ctx context.Context, tasks []T, workers int) error { 25 | if len(tasks) == 0 || workers == 0 { 26 | return nil 27 | } 28 | 29 | ctx, cancel := context.WithCancel(ctx) 30 | defer cancel() 31 | 32 | // Revise the number of workers. 33 | workers = min(workers, len(tasks)) 34 | 35 | // Create a semaphore to limit the number of concurrent workers. 36 | semaphore := make(chan struct{}, workers) 37 | for i := 0; i < workers; i++ { 38 | semaphore <- struct{}{} 39 | } 40 | 41 | // Create a channel to collect errors from tasks. 42 | errCh := make(chan error, workers) 43 | 44 | // Define a worker function that executes a task. 45 | workerFunc := func(t Task) bool { 46 | select { 47 | case <-ctx.Done(): 48 | return false 49 | case <-semaphore: 50 | go func() { 51 | // Release the semaphore when the task is done. 52 | defer func() { 53 | semaphore <- struct{}{} 54 | }() 55 | 56 | if err := t.Execute(ctx); err != nil { 57 | // Cancel the context if any task fails. 58 | cancel() 59 | if !errors.Is(err, context.Canceled) { 60 | errCh <- err 61 | } 62 | } 63 | }() 64 | return true 65 | } 66 | } 67 | 68 | // Execute each task concurrently. 69 | for _, task := range tasks { 70 | if !workerFunc(task) { 71 | break 72 | } 73 | } 74 | 75 | // Drain the semaphore. 76 | for i := 0; i < workers; i++ { 77 | <-semaphore 78 | } 79 | close(errCh) 80 | close(semaphore) 81 | 82 | // Return the first error that occurs. 83 | nErrors := len(errCh) 84 | if nErrors > 0 { 85 | err := <-errCh 86 | for range errCh { 87 | // Drain the error channel to make it garbage collected. 88 | } 89 | return fmt.Errorf( 90 | "failed to execute concurrently %d tasks, first error: %w", 91 | len(tasks), 92 | err, 93 | ) 94 | } 95 | 96 | if ctx.Err() != nil { 97 | return ctx.Err() 98 | } 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /concurrency/tasks/tasks_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "testing" 7 | "time" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestExecuteTasks(t *testing.T) { 13 | ctx := context.Background() 14 | 15 | t.Run("No tasks", func(t *testing.T) { 16 | err := ExecuteTasks(ctx, []TaskFunc{}, 0) 17 | assert.NoError(t, err) 18 | }) 19 | 20 | t.Run("All tasks succeed", func(t *testing.T) { 21 | tasks := []TaskFunc{ 22 | func(ctx context.Context) error { 23 | return nil 24 | }, 25 | func(ctx context.Context) error { 26 | return nil 27 | }, 28 | func(ctx context.Context) error { 29 | return nil 30 | }, 31 | } 32 | 33 | err := ExecuteTasks(ctx, tasks, len(tasks)) 34 | assert.NoError(t, err) 35 | }) 36 | 37 | t.Run("All tasks failed", func(t *testing.T) { 38 | expectedErr := errors.New("task failed") 39 | 40 | tasks := []TaskFunc{ 41 | func(ctx context.Context) error { 42 | return expectedErr 43 | }, 44 | func(ctx context.Context) error { 45 | return expectedErr 46 | }, 47 | func(ctx context.Context) error { 48 | return expectedErr 49 | }, 50 | } 51 | 52 | err := ExecuteTasks(ctx, tasks, len(tasks)) 53 | assert.ErrorIs(t, err, expectedErr) 54 | assert.ErrorContains( 55 | t, err, "failed to execute concurrently 3 tasks, first error: task failed", 56 | ) 57 | }) 58 | 59 | t.Run("One task fails", func(t *testing.T) { 60 | expectedErr := errors.New("task failed") 61 | 62 | tasks := []TaskFunc{ 63 | func(ctx context.Context) error { 64 | return nil 65 | }, 66 | func(ctx context.Context) error { 67 | return expectedErr 68 | }, 69 | func(ctx context.Context) error { 70 | return nil 71 | }, 72 | } 73 | 74 | err := ExecuteTasks(ctx, tasks, len(tasks)) 75 | assert.ErrorIs(t, err, expectedErr) 76 | assert.ErrorContains( 77 | t, err, "failed to execute concurrently 3 tasks, first error: task failed", 78 | ) 79 | }) 80 | 81 | t.Run("Context already canceled", func(t *testing.T) { 82 | expectedErr := context.Canceled 83 | 84 | tasks := []TaskFunc{ 85 | func(ctx context.Context) error { 86 | return nil 87 | }, 88 | func(ctx context.Context) error { 89 | return nil 90 | }, 91 | func(ctx context.Context) error { 92 | return nil 93 | }, 94 | } 95 | 96 | cancelCtx, cancel := context.WithCancel(ctx) 97 | cancel() 98 | 99 | err := ExecuteTasks(cancelCtx, tasks, len(tasks)) 100 | assert.ErrorIs(t, err, expectedErr) 101 | }) 102 | 103 | t.Run("One task is canceled", func(t *testing.T) { 104 | expectedErr := context.Canceled 105 | 106 | tasks := []TaskFunc{ 107 | func(ctx context.Context) error { 108 | return sleepWithCtx(ctx, time.Second) 109 | }, 110 | func(ctx context.Context) error { 111 | return nil 112 | }, 113 | func(ctx context.Context) error { 114 | return nil 115 | }, 116 | } 117 | 118 | cancelCtx, cancel := context.WithCancel(ctx) 119 | go func() { 120 | time.Sleep(time.Millisecond) 121 | cancel() 122 | }() 123 | 124 | err := ExecuteTasks(cancelCtx, tasks, len(tasks)) 125 | assert.ErrorIs(t, err, expectedErr) 126 | }) 127 | 128 | t.Run("All tasks are canceled", func(t *testing.T) { 129 | expectedErr := context.Canceled 130 | 131 | tasks := []TaskFunc{ 132 | func(ctx context.Context) error { 133 | return sleepWithCtx(ctx, time.Second) 134 | }, 135 | func(ctx context.Context) error { 136 | return sleepWithCtx(ctx, 2*time.Second) 137 | }, 138 | func(ctx context.Context) error { 139 | return sleepWithCtx(ctx, 2*time.Second) 140 | }, 141 | } 142 | 143 | cancelCtx, cancel := context.WithCancel(ctx) 144 | go func() { 145 | time.Sleep(time.Millisecond) 146 | cancel() 147 | }() 148 | 149 | err := ExecuteTasks(cancelCtx, tasks, len(tasks)) 150 | assert.ErrorIs(t, err, expectedErr) 151 | }) 152 | 153 | t.Run("One task fails others are canceled", func(t *testing.T) { 154 | expectedErr := errors.New("task failed") 155 | 156 | tasks := []TaskFunc{ 157 | func(ctx context.Context) error { 158 | return sleepWithCtx(ctx, time.Second) 159 | }, 160 | func(ctx context.Context) error { 161 | return expectedErr 162 | }, 163 | func(ctx context.Context) error { 164 | return sleepWithCtx(ctx, time.Second) 165 | }, 166 | } 167 | 168 | cancelCtx, cancel := context.WithCancel(ctx) 169 | go func() { 170 | time.Sleep(time.Millisecond) 171 | cancel() 172 | }() 173 | 174 | err := ExecuteTasks(cancelCtx, tasks, len(tasks)) 175 | assert.ErrorIs(t, err, expectedErr) 176 | }) 177 | 178 | t.Run("Tasks take longer than the context deadline", func(t *testing.T) { 179 | expectedErr := context.DeadlineExceeded 180 | 181 | tasks := []TaskFunc{ 182 | func(ctx context.Context) error { 183 | return sleepWithCtx(ctx, time.Second) 184 | }, 185 | func(ctx context.Context) error { 186 | return sleepWithCtx(ctx, time.Second) 187 | }, 188 | func(ctx context.Context) error { 189 | return sleepWithCtx(ctx, time.Second) 190 | }, 191 | } 192 | 193 | deadline := time.Now().Add(10 * time.Millisecond) 194 | ctx, cancel := context.WithDeadline(ctx, deadline) 195 | defer cancel() 196 | 197 | err := ExecuteTasks(ctx, tasks, len(tasks)) 198 | assert.ErrorIs(t, err, expectedErr) 199 | }) 200 | 201 | t.Run("Limit the number of workers", func(t *testing.T) { 202 | taskDuration := 10 * time.Millisecond 203 | 204 | // Create a list of tasks that sleep for 50 milliseconds. 205 | tasks := []TaskFunc{ 206 | func(ctx context.Context) error { 207 | return sleepWithCtx(ctx, taskDuration) 208 | }, 209 | func(ctx context.Context) error { 210 | return sleepWithCtx(ctx, taskDuration) 211 | }, 212 | func(ctx context.Context) error { 213 | return sleepWithCtx(ctx, taskDuration) 214 | }, 215 | func(ctx context.Context) error { 216 | return sleepWithCtx(ctx, taskDuration) 217 | }, 218 | } 219 | 220 | // Measure the time it takes to execute all tasks. 221 | start := time.Now() 222 | err := ExecuteTasks(ctx, tasks, 2) 223 | elapsed := time.Since(start) 224 | 225 | assert.NoError(t, err) 226 | // The total time should be around 2 times the task duration. 227 | assert.InDelta(t, elapsed, 2*taskDuration, float64(taskDuration/2)) 228 | }) 229 | 230 | t.Run("Tasks are canceled with workers limit", func(t *testing.T) { 231 | expectedErr := context.Canceled 232 | 233 | tasks := []TaskFunc{ 234 | func(ctx context.Context) error { 235 | return sleepWithCtx(ctx, time.Second) 236 | }, 237 | func(ctx context.Context) error { 238 | return sleepWithCtx(ctx, time.Second) 239 | }, 240 | func(ctx context.Context) error { 241 | return sleepWithCtx(ctx, time.Second) 242 | }, 243 | func(ctx context.Context) error { 244 | return sleepWithCtx(ctx, time.Second) 245 | }, 246 | } 247 | 248 | cancelCtx, cancel := context.WithCancel(ctx) 249 | go func() { 250 | time.Sleep(time.Millisecond) 251 | cancel() 252 | }() 253 | 254 | err := ExecuteTasks(cancelCtx, tasks, 2) 255 | assert.ErrorIs(t, err, expectedErr) 256 | }) 257 | } 258 | 259 | // sleepWithCtx sleeps for the given duration or until the context is canceled. 260 | func sleepWithCtx(ctx context.Context, duration time.Duration) error { 261 | select { 262 | case <-ctx.Done(): 263 | return ctx.Err() 264 | case <-time.After(duration): 265 | return nil 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module pavel-fokin/go-patterns 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/davecgh/go-spew v1.1.1 // indirect 7 | github.com/pmezard/go-difflib v1.0.0 // indirect 8 | github.com/stretchr/testify v1.9.0 // indirect 9 | gopkg.in/yaml.v3 v3.0.1 // indirect 10 | ) 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 6 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 7 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 8 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 9 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 10 | -------------------------------------------------------------------------------- /minimal-docker/Dockerfile.large: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine 2 | 3 | RUN adduser \ 4 | --disabled-password \ 5 | --gecos "" \ 6 | --home "/nonexistent" \ 7 | --shell "/sbin/nologin" \ 8 | --no-create-home \ 9 | --uid 65532 \ 10 | user 11 | 12 | WORKDIR /app 13 | COPY . . 14 | 15 | RUN go mod download 16 | RUN go mod verify 17 | 18 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /server . 19 | 20 | USER user:user 21 | 22 | EXPOSE 8080 23 | 24 | CMD ["/server"] 25 | -------------------------------------------------------------------------------- /minimal-docker/Dockerfile.small: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine as golang 2 | 3 | WORKDIR /app 4 | COPY . . 5 | 6 | RUN go mod download 7 | RUN go mod verify 8 | 9 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /server . 10 | 11 | FROM gcr.io/distroless/static-debian11 12 | 13 | COPY --from=golang /server . 14 | 15 | EXPOSE 8080 16 | 17 | CMD ["/server"] 18 | -------------------------------------------------------------------------------- /minimal-docker/Dockerfile.xsmall: -------------------------------------------------------------------------------- 1 | FROM golang:1.18-alpine as golang 2 | 3 | RUN apk add -U tzdata 4 | RUN apk --update add ca-certificates 5 | 6 | WORKDIR /app 7 | COPY . . 8 | 9 | RUN go mod download 10 | RUN go mod verify 11 | 12 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /server . 13 | 14 | FROM scratch 15 | 16 | COPY --from=golang /usr/share/zoneinfo /usr/share/zoneinfo 17 | COPY --from=golang /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 18 | COPY --from=golang /etc/passwd /etc/passwd 19 | COPY --from=golang /etc/group /etc/group 20 | 21 | COPY --from=golang /server . 22 | 23 | EXPOSE 8080 24 | 25 | CMD ["/server"] 26 | -------------------------------------------------------------------------------- /minimal-docker/go.mod: -------------------------------------------------------------------------------- 1 | module pavel-fokin/helloworld 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /minimal-docker/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | func main() { 8 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){ 9 | w.Write([]byte("Hello, World!")) 10 | }) 11 | 12 | http.ListenAndServe(":8000", nil) 13 | } 14 | -------------------------------------------------------------------------------- /web/embeded/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Like It! 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /web/embeded/frontend/static/app.js: -------------------------------------------------------------------------------- 1 | function App() { 2 | return ( 3 | 8 | ) 9 | } 10 | 11 | const root = ReactDOM.createRoot(document.getElementById("app")); 12 | root.render(); 13 | -------------------------------------------------------------------------------- /web/embeded/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "embed" 5 | "io" 6 | "io/fs" 7 | "log" 8 | "net/http" 9 | ) 10 | 11 | var ( 12 | //go:embed frontend 13 | content embed.FS 14 | 15 | //go:embed frontend/index.html 16 | index string 17 | ) 18 | 19 | func main() { 20 | static, _ := fs.Sub(fs.FS(content), "frontend") 21 | 22 | http.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) { 23 | io.WriteString(w, index) 24 | }) 25 | http.Handle( 26 | "/static/", http.FileServer(http.FS(static))) 27 | 28 | log.Fatal(http.ListenAndServe(":8080", nil)) 29 | } 30 | -------------------------------------------------------------------------------- /web/gracefulshutdown/README.md: -------------------------------------------------------------------------------- 1 | # Graceful shutdown -------------------------------------------------------------------------------- /web/gracefulshutdown/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os/signal" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | const maxShutdownDuration = 30 * time.Second 13 | 14 | func main() { 15 | // Listen for OS signals. 16 | ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) 17 | defer stop() 18 | 19 | // Create a web server. 20 | httpServer := &http.Server{ 21 | Addr: ":8000", 22 | } 23 | 24 | // Start the server. 25 | go func() { 26 | log.Println("Start server...") 27 | if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { 28 | log.Fatalf("Server error: %v\n", err) 29 | } 30 | }() 31 | 32 | <-ctx.Done() 33 | 34 | ctx, cancel := context.WithTimeout(context.Background(), maxShutdownDuration) 35 | defer cancel() 36 | 37 | // Shutdown the server gracefully. 38 | log.Println("Shutdown server...") 39 | if err := httpServer.Shutdown(ctx); err != nil { 40 | log.Fatalf("Failed to shutdown the server gracefully: %v\n", err) 41 | } 42 | } 43 | --------------------------------------------------------------------------------