Advanced features and Sofware Craft

SHODO

Hello! I'm Nathan.

What about you?

The next four hours

Welcome to the "Advanced features and Software Craft" training

This presentation is in English. Many terms do not have a proper translation in French.

 

This is my early stages as Go trainer. If you have any question at any time, feel free to ask.

Schedule

Morning session

  • 9h: Session start
  • 10h: 5 minutes break
  • 11h: 10 minutes break
  • 12h: 5 minutes break
  • 13h: Session ends

Afternoon session

  • 14h: Session start
  • 15h: 5 minutes break
  • 16h: 10 minutes break
  • 17h: 5 minutes break
  • 18h: Session ends

What do you expect

from this training?

Content

  • Generics
  • Interfaces
  • Software Craft & Code Design
  • (bonus) Concurrency

A four-hours training means ...

  • A lot of resources will be provided to go further outside of the training session
  • Some topics are excluded from this training session
  • Sometimes, it's my point of view
  • I'm available for any question following the session: nathan.castelein@shodo-lille.io

Prerequisites

  • Go v1.22
  • Visual Studio Code
  • Git

Generics

Generics

Also known as parametric polymorphism, this concept allows a single piece of code to be given a "generic" type, using variables in place of actual types, and then instantiated with particular types as needed.

 

It forms the basis of generic programming.

Let's start with a simple exercise

Let's refresh our memory by writing simple functions and unit tests!

File to open:

generics/exercise1.md

A simple example

func ContainsString(slice []string, elem string) bool {
	for _, sliceElem := range slice {
		if sliceElem == elem {
			return true
		}
	}

	return false
}

func ContainsInt(slice []int, elem int) bool {
	for _, sliceElem := range slice {
		if sliceElem == elem {
			return true
		}
	}

	return false
}

A bit redundant, isn't it?

Introducing Generics in Go

func Count[T comparable](slice []T, elem T) int {
	count := 0
	for _, sliceElem := range slice {
    	if sliceElem == elem {
        	count++
        }
    }
    
    return count
}

Since Go 1.18, generics have been integrated the language.

Focus on function signature

func Count
          [T
             comparable]
                        (slice []T, 
                                    elem T)
                                            int {
}

Let's have a focus on function signature

Type constraints

Type constraints in generics are interfaces required to be matched by the types.

 

It defines an expected behaviour for the types, so we can perform actions on it. Typical used constraints:

Type constraints

func Sum[T int64 | float64](slice []T) T {
	var total T
	for _, element := range slice {
		total += element
	}

	return total
}

You can create an union of multiple constraints using the pipe "|" char.

Write your first generic function

Let's use generics to rewrite our function!

File to open:

generics/exercise1.md

Write your first generic function

func Contains[T comparable](slice []T, elem T) bool {
	for _, sliceElem := range slice {
		if sliceElem == elem {
			return true
		}
	}

	return false
}

func ContainsMap[K, V comparable](m map[K]V, elem V) bool {
	for _, mapElem := range m {
		if mapElem == elem {
			return true
		}
	}

	return false
}

Generics in struct declaration

Generics can be used in function declaration, but they can also be used for structs.

 

We can easily extend data structures with new helpers functions: an ordered map, a queue, a list, etc.

 

Let's have an example with maps.

Generics in struct

type Map[Key comparable, Value any] struct {
	store map[Key]Value
}

func NewMap[Key comparable, Value any]() *Map[Key, Value] {
	return &Map[Key, Value]{
		store: map[Key]Value{},
	}
}

Generics in struct

func (m Map[Key, Value]) Keys() []Key {
	keys := make([]Key, len(m.store))
	var i int
	for key := range m.store {
		keys[i] = key
		i++
	}

	return keys
}

func (m *Map[Key, Value]) Store(key Key, value Value) {
	m.store[key] = value
}

Generics in struct

func TestMapKeys(t *testing.T) {
	// Arrange
	m := NewMap[string, int]()
	m.Store("hello", 1)
	m.Store("world", 2)

	// Act
	keys := m.Keys()

	// Assert
	require.Len(t, keys, 2)
	require.Equal(t, []string{"hello", "world"}, keys)
}

A generic queue?

A Queue has been implemented!

But it works only with int, it's a shame isn't it?

 

Let's rewrite a bit with generics!

File to open:

generics/exercise2.md

A generic queue

type Queue[T any] struct {
	elements []T
}

func (f *Queue[T]) Push(element T) {
	f.elements = append(f.elements, element)
}

func (f *Queue[T]) Pop() (T, error) {
	if len(f.elements) == 0 {
		var zeroValue T
		return zeroValue, errors.New("no more elements")
	}
	element, newSlice := f.elements[0], f.elements[1:]
	f.elements = newSlice
	return element, nil
}

When to use generics?

https://go.dev/blog/when-generics

If you find yourself writing the exact same code multiple times, where the only difference is that the code uses different types, you can use a generic type parameter.

Avoid type parameters until you notice that you are about to write the exact same code multiple times.

 

Tips:

  • Generics are good with data structures
  • Start by writing code, not generics
  • Rewrite with generics if needed, not by design

Generics example

You have many examples of Generics in this library: https://github.com/samber/lo?tab=readme-ov-file#-spec

 

Consider reading this library and reimplementing some stuffs for your needs!

Interfaces

Interfaces

An interface type is defined as a set of method signatures.

 

A value of interface type can hold any value that implements those methods.

Interfaces

Interfaces are one of the strongest feature of the language.

 

Interfaces are implemented implicitly. If your struct implements the proper methods, then the interface is satisfied and your type can be used as the interface.

 

If a function or a method requires an interface to work with, then you can easily write your own implementation. 

Known interfaces

Example

type Speaker interface {
	Speak() string
}

type Cat struct{}

func (c Cat) Speak() string {
	return "Meow!"
}

type Dog struct{}

func (d Dog) Speak() string {
	return "Wouf!"
}

Example

func MakeSomeNoise(animals []Speaker) {
	for _, animal := range animals {
		fmt.Println(animal.Speak())
	}
}

// in _test.go

func TestMakeSomeNoise(t *testing.T) {
    // ...
    
    // Act
    MakeSomeNoise([]Speaker{
		Cat{},
		Dog{},
		Cat{},
	})
    
    // Will print "Meow!", "Wouf!", "Meow!"
}

A store of compute and storage

You're now owner of some code of an online store which sells compute and storage.

 

There's a code smell in the code: looks like it can be refreshed with an interface!

File to open:

interfaces/store/exercise1.md

A store of compute and storage

type Compute struct {
	NumberOfVCPUs int
}

func (c Compute) GetPrice() float64 {
	unitPrice := 3.0
	return float64(c.NumberOfVCPUs) * unitPrice
}
type Storage struct {
	QuantityInGB float64
}

func (s Storage) GetPrice() float64 {
	var unitPrice float64
	switch {
	case s.QuantityInGB >= 1024*10:
		unitPrice = 1.0
    case s.QuantityInGB >= 1024:
		unitPrice = 1.5
	default:
		unitPrice = 2.0
	}

	return unitPrice * s.QuantityInGB
}
type Pricer interface {
	GetPrice() float64
}

A store of compute and storage

type Basket struct {
	Products []Pricer
}

func (b Basket) GetTotalPrice() float64 {
	totalPrice := 0.0
	for _, product := range b.Products {
		totalPrice += product.GetPrice()
	}

	return totalPrice
}

func (b *Basket) AddProduct(product Pricer) {
	b.Products = append(b.Products, product)
}

Check if your structure matches a specific interface

var (
	_ Pricer = &Storage{}
)

var (
	_ Pricer = &Compute{}
)

To confirm your structure properly implements a given interface, you can add a simple snippet into your code. This will help to detect any issue on interface implementation in your package.

A store of compute and storage

Unit tests

func TestGetTotalPrice(t *testing.T) {
	// Arrange
	compute := Compute{NumberOfVCPUs: 4}   // total price is 12
	storage := Storage{QuantityInGB: 2000} // total price is 3000
	basket := Basket{Products: []Pricer{compute, storage}}
	expectedTotalPrice := 3012.0

	// Act
	totalPrice := basket.GetTotalPrice()

	// Assert
	if expectedTotalPrice != totalPrice {
		t.Fatalf("expected total price %.2f, got %.2f", expectedTotalPrice, totalPrice)
	}
}

Something wrong with this test?

Testing Basket structure

For now, if you change the business rule to get the price of storage for example, then you have to modify the unit test of your Basket's GetTotalPrice method.

 

I'd not recommend to have this kind of design! Let's write a custom Pricer for our unit tests.

File to open:

interfaces/store/exercise2.md

Testing Basket structure

type StubPricer struct{}

func (StubPricer) GetPrice() float64 {
	return 1.0
}

func TestGetTotalPriceWithStub(t *testing.T) {
	// Arrange
	basket := Basket{Products: []Pricer{StubPricer{}, StubPricer{}, StubPricer{}, StubPricer{}}}
	expectedTotalPrice := 4.0

	// Act
	totalPrice := basket.GetTotalPrice()

	// Assert
	if expectedTotalPrice != totalPrice {
		t.Fatalf("expected total price %.2f, got %.2f", expectedTotalPrice, totalPrice)
	}
}

Interfaces in unit tests

Interfaces, when wisely used, can be a strong feature to keep your unit tests independent and properly scoped.

For this exercise, we just created a stub pricer, following the Test Double pattern we discussed in the previous module about tests (https://martinfowler.com/bliki/TestDouble.html).

 

It gives us a full control of the object's behaviour, so we test only the algorithm of GetTotalPrice.

To go this way, remind that you need to create unit tests for Compute and Storage structure!

Interface composition

type ReadWriter interface {
        Reader
        Writer
}

An interface can be composed of ... an addition of other interfaces!

 

If you want to meet the ReadWriter interface, you need to meet the interface of Reader, and Writer.

Interfaces mindset

Interfaces should not embed an infinite list of methods. Prefer small interfaces and a bit of composed interfaces. Prefer naming interfaces for their behaviour, not by entity!

 

The bigger the interface, the weaker the abstraction.

Rob Pike

 

Clients should not be forced to depend on methods they do not use.

Robert C. Martin 

Software Craft and code design

Software Craft?

The path to write good code

Let's start with Agile

What's Agile Software Development?

Agile in 2001

The Agile Software Development concept has been written down by 17 software developers in February 2001.

 

They created the Agile Manifesto.

Agile Manifesto

We are uncovering better ways of developing software by doing it and helping others do it. Through this work we have come to value:

 

Individuals and interactions over processes and tools
Working software over comprehensive documentation
Customer collaboration over contract negotiation
Responding to change over following a plan

 

That is, while there is value in the items on
the right, we value the items on the left more.

And?

And that's all!

 

Scrum, Kanban, SAFE, etc. are just (good or bad) ways to achieve this goal. They're not agility, they're just tools!

 

You can have a look on this keynote by Alexandre Boutin: https://www.youtube.com/watch?v=Wbr-eceBvto

And Software Craftsmanship?

Almost ten years after the Agile Manifesto, a group around Bob Martin wrote an extension to it: the Software Craftsmanship Manifesto (ie. Software Craft).

 

This document has been based on the work of various people who worked on software design and quality since many years (with books like Pragmatic Programmers in 1999, Software Craftsmanship in 2001, etc.).

Software Craft Manifesto

As aspiring Software Craftsmen we are raising the bar of professional software development by practicing it and helping others learn the craft. Through this work we have come to value:

 

Not only working software, but also well-crafted software
Not only responding to change, but also steadily adding value
Not only individuals and interactions, but also a community of professionals
Not only customer collaboration, but also productive partnerships


That is, in pursuit of the items on the left we have found the items on the right to be indispensable.

Software Craft

Software craft is the idea of pushing back the developer in the center of Software Development.

 

It is a response by software developers to the perceived ills of the mainstream software industry, including the prioritization of financial concerns over developer accountability.

A toolbox

Following this guide, Software Craft can be seen as a toolbox of practices, designs, ways of working together and self-improvement.

 

For example: TDD, BDD, DDD, Clean Architecture, Hexagonal Architecture, SOLID principles, Example Mapping, Event Storming, Pair Programming, Mob Programming/Software Teaming, etc.

 

Software Craft covers much more than writing code.

Software Craft in a Go training?

Regarding this training, we will mainly focus on some important design patterns we can use with Go.

 

It's a small part of Software Craft, but I hope I'll create some curiosity about those practices!

SOLID principles

SOLID principles

Designed by Robert C. Martin in the early 2000's, SOLID groups five design principles intended to make object-oriented designs more understandable, flexible, and maintainable.

 

It is an acronym!

 

Thanks to interfaces, it's easy to follow those principles.

SOLID Principles

Signs of bad code we want to get rid of with SOLID principles:

  • Rigidity: Code to be difficult to change even if the change is small
  • Fragility: Code to break whenever a new change is introduced in the system
  • Immobility: Code being not reusable
  • Viscosity: Hacking rather than finding a solution that preserve the design when it comes to change

Single-responsibility principle

type User struct {
    FirstName string
    LastName  string
    Email     string
}

func (u *User) ChangeEmail(newEmail string) {
    u.Email = newEmail
}

func (u *User) GetFullName() string {
    return u.FirstName + " " + u.LastName
}

func (u *User) Save() error {
    // Save user to the database
}

Single-responsibility principle

type UserRepository struct {
	db *sql.DB
}

func (u *UserRepository) Save(user User) error {
    // Save user to the database
}

There should never be more than one reason for a class to change. In other words, every class should have only one responsibility.

Do one thing, and do it well.

type User struct {
    FirstName string
    LastName  string
    Email     string
}

func (u *User) ChangeEmail(newEmail string) {
    u.Email = newEmail
}

func (u *User) GetFullName() string {
    return u.FirstName + " " + u.LastName
}

Open-Closed Principle (OCP)

type PaymentMethod struct {
	Type string
}

func MakePayment(paymentMethod PaymentMethod) {
	switch paymentMethod.Type  {
    case "CreditCard":
    	// Pay by credit card
    case "PayPal":
        // Pay by PayPal
    ...
    }
}

Open-Closed Principle (OCP)

type Payer interface {
	Pay()
}

func MakePayment(paymentMethod Payer) {
	paymentMethod.Pay()
}

type CreditCard struct {}

func (c *CreditCard) Pay() {
	...
}

type PayPal struct {}

func (p *PayPal) Pay() {
	...
}

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Liskov substitution principle (LSP)

type Swimmer interface {
	Swim() string
}

type Shark struct{}

func (*Shark) Swim() string {
    return "Shark is swimming."
}

func PoolParty(s Swimmer) string {
    return s.Swim()
}

In an object-oriented class-based language, the concept of the Liskov substitution principle is that a user of a base class should be able to function properly of all derived classes.

Interface Segregation Principle (ISP)

type Bird interface {
    Fly() string
    Sing() string
}

type Pigeon struct{}

func (p *Pigeon) Fly() string {
    return "Pigeon is flying."
}

func (p *Pigeon) Sing() string {
    return "Pigeon is singing."
}

type Penguin struct{}

func (p *Penguin) Fly() string {
	panic("I can't fly")
}

func (p *Penguin) Sing() string {
    return "Penguin is singing."
}

Interface Segregation Principle (ISP)

This principle states that clients should not be forced to depend on interfaces they do not use.

 

If we violate this principle, we may have interfaces that are too large and contain methods that are not relevant to some clients, which can lead to code that is difficult to understand and maintain.

type Singer interface {
    Sing() string
}

type Flyer interface {
    Fly() string
}

type Pigeon struct{}

func (p *Pigeon) Fly() string {
    return "Pigeon is flying."
}

func (p *Pigeon) Sing() string {
    return "Pigeon is singing."
}

type Penguin struct{}

func (p *Penguin) Sing() string {
    return "Penguin is screaming."
}

Interface Segregation Principle (ISP)

type Command interface {
     Execute() ([]byte, error)
     ValidateInput() bool
}

Split your interfaces by behaviour or by clients, not by entities.

 

This principle encourages creating smaller, more focused interfaces rather than large, monolithic ones.

type Command interface {
     Execute() ([]byte, error)
}
type CommandWithInput interface {
     Command
     ValidateInput() bool
}

Don't

Do

Dependency Inversion Principle (DIV)

type Mail struct {
	from    string
    to      []string
    message string
}

func (m Mail) Send() error {
	return smtp.SendMail("ssl0.ovh.net:587", "smtp-authentication", m.from, m.to, m.message)
}

type NotificationService struct {}

func (n NotificationService) Notify(m Mail) error {
	// Do some stuff
	return m.Send()
}

Dependency Inversion Principle (DIV)

The Dependency Inversion Principle states that high-level modules should not depend on low-level modules, but both should depend on abstractions.

 

This principle promotes loose coupling between components, making the code more maintainable and testable.

Dependency Inversion Principle (DIV)

type Sender interface {
	Send() error
}

type Mail struct {
	from    string
    to      []string
    message string
}

func (m Mail) Send() error {
	return smtp.SendMail("ssl0.ovh.net:587", "smtp-authentication", m.from, m.to, m.message)
}

type NotificationService struct {}

func (n NotificationService) Notify(s Sender) error {
	// Do some stuff
	return s.Send()
}

Dependency Inversion Principle (DIV)

This principle is probably the most important of SOLID principles.

 

Strictly following this one will help you to keep a strong codebase, reactive to changes.

Interfaces

Interfaces will be a key feature to implement SOLID principles in our code.

 

It will help to loose coupling between components in a proper way.

 

Let's have an example!

A SOLID API

Let's understand how to properly use interfaces to separate different components of an application and loose coupling.

 

First of all, let's have a look on the existing codebase.

File to open:

craft/exercise1.md

adapters package

type UserSQL struct {
	db *sql.DB
}

func NewUserSQL(db *sql.DB) user.Lister {
	return &UserSQL{db: db}
}

func (u *UserSQL) List() ([]user.User, error) {
	rows, err := u.db.Query(`SELECT firstname, lastname FROM users`)
	if err != nil {
		return nil, err
	}
	defer rows.Close()

	users := make([]user.User, 0)
	for rows.Next() {
		var user user.User
		if err := rows.Scan(&user.Firstname, &user.Lastname); err != nil {
			return nil, err
		}

		users = append(users, user)
	}

	return users, nil
}

user package

type Lister interface {
	List() ([]User, error)
}
type User struct {
	Firstname string
	Lastname  string
}

port.go

user.go

Fix the upper layer

Now that our user package is properly fixed, let's have a look on the http package.

File to open:

craft/exercise2.md

Fix the upper layer

type Server struct {
	listeningPort int
	router        *echo.Echo
	user          user.Lister
}

func NewServer(listeningPort int, user user.Lister) *Server {
	router := echo.New()
	server := &Server{
		listeningPort: listeningPort,
		router:        router,
		user:          user,
	}
	router.GET("/user", server.GetUsers)
	return server
}

Fix the upper layer

func (s *Server) ListUsers(ctx echo.Context) error {
	users, err := s.user.List()
	if err != nil {
		return ctx.JSON(http.StatusBadRequest, err.Error())
	}

	return ctx.JSON(http.StatusOK, users)
}

Fixing the unit test?

Now we have to fix the unit test.

 

How to do it properly?

With a stub!

File to open:

craft/exercise3.md

Fixing the unit test

type UserListerStub struct{}

func (u *UserGetterStub) List() ([]user.User, error) {
	return []user.User{
		{
			Firstname: "SpongeBob",
			Lastname:  "SquarePants",
		},
		{
			Firstname: "Patrick",
			Lastname:  "Star",
		},
	}, nil
}

var (
	_ user.Lister = &UserListerStub{}
)

Fixing the unit test

func TestListUsers(t *testing.T) {
	// Arrange
	e := echo.New()
	req := httptest.NewRequest(http.MethodGet, "/user", nil)
	rec := httptest.NewRecorder()
	c := e.NewContext(req, rec)
	c.SetPath("/user")

	server := &Server{user: &UserListerStub{}}

	// Act
	err := server.ListUsers(c)

	// Assert
	require.NoError(t, err)
	require.Equal(t, http.StatusOK, rec.Code)
	require.JSONEq(t, 
    	`[{"Firstname": "SpongeBob", "Lastname": "SquarePants"}, {"Firstname": "Patrick", "Lastname": "Star"}]`, 
        rec.Body.String())
}

Take a step back

With some simple usages of interfaces, we just removed a lot of coupling between our components.

 

Unit tests are often a really good indicator of the stickiness of your components together.

Even if it seems a bit overkill, this will help you to maintain a reliable codebase.

 

And (probably) without noticing, you just use a strong architecture pattern: the hexagonal architecture!

Hexagonal Architecture

Hexagonal Architecture

Introduced by Alistair Cockburn (Agile Manifesto & Software Craft Manifesto) in 2005.

 

The Hexagonal Architecture aims at creating loosely coupled application components that can be easily connected to their software environment by means of ports and adapters. This makes components exchangeable at any level and facilitates test automation.

Hexagonal Architecture

Hexagonal Architecture

Goals

The application should be equally controllable by users, other applications, or automated tests.

 

For the business logic, it makes no difference whether it is invoked from a user interface, a REST API, or a test framework.

Goals

The business logic should be able to be developed and tested in isolation from the database, other infrastructure, and third-party systems.

 

From a business logic perspective, it makes no difference whether data is stored in a relational database, a NoSQL system, XML files, or proprietary binary format.

Goals

Infrastructure modernization (e.g., upgrading the database server, adapting to changed external interfaces, upgrading insecure libraries) should be possible without adjustments to the business logic.

Let's have a look on some code

Folder to open:

solution/hexagonal/

Conclusion

Hexagonal Architecture is one of the many tools of the Software Craft perspective !

 

Using this port/adapter pattern can be a game changer for large codebase.

 

And Go is providing a proper way to do it thanks to interfaces!

Project organization

One layout to rule them all?

Go provides a small blog article to organize your code: https://go.dev/doc/modules/layout

 

There's also a non-official layout is proposed here: https://github.com/golang-standards/project-layout

 

This repository provides tips and usage on how properly create a new project in Go, based on what is recommended on the standard library, and what can be found in famous Go projects.

The internal directory

The internal directory is a feature introduced in Go 1.4, to limit the exposition of some functions to the outside.

 

It's a good way to define clear boundaries and avoid the usage of some pieces of code you want to keep internal to your package, without put those pieces as private.

The internal package

/api
/pkg
    /account
	/internal
    	/user

In this example, the user package:

  • can be imported in pkg and account package
  • cannot be imported in api package

This mechanism helps to manage the exposition of your code.

Concurrency

Concurrency

Concurrency in computing enables different parts of a program to execute independently, potentially improving performance and allowing better use of system resources.

 

Concurrency is the composition of independently executing tasks. It’s about dealing with a lot of things at once.

 

Concurrency is a way to structure a program by breaking it into pieces that can be executed independently.

Concurrency is not Parallelism

Parallelism is about doing a lot of things at once.

 

It’s a subset of concurrency, where the execution of tasks literally happens at the same time, like splitting the data processing task over multiple CPU cores. 

 

In programming, concurrency is the composition of independently executing processes, while parallelism is the simultaneous execution of (possibly related) computations.

Adding concurrency

Adding parallelism

goroutines

A Goroutine is a lightweight thread of execution.

 

The term comes from the phrase “Go subroutine”, reflecting the fact that Goroutines are functions or methods that run concurrently with others. 

Starting a goroutine

package main

import "fmt"

func HelloWorld() {
	fmt.Println("Hello world from my function!")
}

func main() {
	go HelloWorld()
    fmt.Println("Hello world from main!")
}

The keyword go starts an execution in a goroutine.

What's the output of this program?

Waiting the end of a subroutine

By default, a goroutine starts a new concurrent process, leaving the main program to continue.

 

At the end of the main program, the program will stop, closing all goroutines even if their work is not finished.

 

Simple solution: let's add a time.Sleep!

Waiting the end of a subroutine

Not really elegant, isn't it?

 

Go provides other ways to do it:

  • sync.WaitGroup
  • channels

sync.WaitGroup

package main

import (
	"fmt"
	"sync"
)

var waitgroup sync.WaitGroup

func HelloWorld() {
	defer waitgroup.Done()
	fmt.Println("Hello world from my function!")
}

func main() {
	waitgroup.Add(1)
	go HelloWorld()

	fmt.Println("Hello world from main!")
	waitgroup.Wait()
}

Channels

package main

import (
	"fmt"
)

func HelloWorld(done chan bool) {
	defer func() {
    	done <- true 
    }()
	fmt.Println("Hello world from my function!")
}

func main() {
	done := make(chan bool)
	go HelloWorld(done)

	fmt.Println("Hello world from main!")
	<-done
}

Channels

Channels in Go are a conduit through which Goroutines communicate and synchronize execution.

 

They can transmit data of a specified type, allowing Goroutines to share information in a thread-safe manner.

 

Notably, they align with Go’s philosophy of “Do not communicate by sharing memory; instead, share memory by communicating.” 

Channels

fmt.Println("receive from a channel")

boolFromChannel := <- channel
fmt.Println("received bool", boolFromChannel)

Channels have two primary operations:

send and receive, denoted by the <- operator.

fmt.Println("send to a channel")
channel <- true

Channel operations are blocking

The send and receive operations are blocking by default.

 

When data is sent to a channel, the control is blocked in the send statement until some other Goroutine reads from that channel.

 

Similarly, if there is no data in the channel, a read from the channel will block the control until some Goroutine writes data to that channel. 

Your first goroutines

Let's add some concurrency with a simple use case, using:

  • WaitGroup
  • Channel

File to open:

concurrency/exercise1.md

WaitGroup

func main() {
	waitgroup := new(sync.WaitGroup)
	for jobNumber := 0; jobNumber < 10; jobNumber++ {
		waitgroup.Add(1)
		go ConcurrentJob(jobNumber, waitgroup)
	}

	waitgroup.Wait()
}

func ConcurrentJob(jobNumber int, waitgroup *sync.WaitGroup) {
	defer waitgroup.Done()
	fmt.Printf("starting job %d\n", jobNumber)
	time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
	fmt.Printf("ending job %d\n", jobNumber)
}

Channels

func main() {
	done := make(chan bool)
	for jobNumber := 0; jobNumber < 10; jobNumber++ {
		go ConcurrentJob(jobNumber, done)
	}

	for jobNumber := 0; jobNumber < 10; jobNumber++ {
		<-done
	}
}

func ConcurrentJob(jobNumber int, done chan bool) {
	defer func() {
		done <- true
	}()
	fmt.Printf("starting job %d\n", jobNumber)
	time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
	fmt.Printf("ending job %d\n", jobNumber)
}

Conclusion

As you might notice, concurrency can be easily done in Go.

 

But it requires a bit of practice to master it.

 

Working on concurrency often requires a bit of attention, and a lot of coffee!

What's next?

Thanks! Questions?

Advanced features and Software Craft

By Nathan Castelein

Advanced features and Software Craft

  • 211