Mutexes
Mutexes are also used to communicate between goroutines and to lock shared resources so that only one goroutine can access them at a time. This helps in avoiding the concurrent read/write problem.
Mutex (mux) stands for mutual exclusion because it blocks all the goroutines except one.
sync.Mutex
The standard Go library provides a built-in mutex as sync.Mutex
, which comes with two methods:
.Lock()
.Unlock()
Only one thread can lock a mutex at once.
An example is given below.
func protected() {
mux.Lock()
defer mux.Unlock()
// ... code here
}
The code block in the above function is protected since there is a mutex. We use the defer
function on unlock, so we never miss unlocking the mutex. When a goroutine calls the above function, the function gets locked. So all other goroutines that call this function get blocked until the initial goroutine finishes executing the function.
Maps are not thread-safe. If there is a map used by multiple goroutines (writing to the map), then using a mutex is a must.
Read write mutex
Go’s standard library provides another type of mutex: sync.RWMutex
.
It has the following methods.
Lock()
Unlock()
RLock()
RUnlock()
Why is this needed? It is not safe to write to a map by multiple goroutines simultaneously. But it is safe to read from a map. When you use Lock()
, no other goroutine is allowed to either read or write that resource. But when RLock()
is used, multiple goroutines are allowed to read the resource. This increases the performance of the application.
RW mutexes allow infinite readers to read a shared resource but only one writer to update it.
Generics
Generics allow us to use variables to refer to specific types. It helps in reducing code duplication. Look at the function signature below.
func splitSlice[T any] (s []T) ([]T, []T) {}
In the above example, T
is the type parameter, and we have mentioned that it can be on any
type. This means the above function can slice int arrays, string arrays, etc. The splitSlice
can be called like below.
s1, s2 := splitSlice([]int{5, 10, 15, 20, 25, 30})
As you can see, we have passed an integer slice to splitSlice()
. So, it returns two integer slices.
Why Generics?
- To reduce repetitive code
- To use in libraries and packages
Constraints
Constraints are used when generic functions are used only for a subset of types. There, we define an interface on which the generic function should operate.
type stringer interface {
String() string
}
func concat[T stringer] (vals []T) string {
// inside this function, you can use
// String() function in stringer interface
}
Parametric Constraints
Interface definitions that can be used as constraints can accept type parameters.
type store[P product] interface {
Sell(P)
}
type product interface {
Price() float64
Name() string
}
Interface Type List
With generics, a new method of writing an interface was also introduced. With that, we can list a set of types to get a new interface or constraint.
type Ordered interface {
~int | ~int8 | ~int16 | ~int32 | ~int64 |
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
~float32 | ~float64 |
~string
}
As you can see, the above interface has no methods but types. But, all those mentioned types support <
, <=
, >
, and >=
operators.
You can create an interface using a type list when you know exactly which types satisfy your interface.
Naming generic types
Check out the function signature given below.
func sendRequest[T any](req T) error {}
Note that T
is just a variable used as the type parameter. You can name it anything you want. But T
is the common version.