Golang is an open-source, compiled, and statically typed programming language designed by Google. It is built to be simple, high-performing, readable, and efficient.
- Golang Types
- Functions vs Methods
- Concurrent programming
- Sync Package
- Type, Interface and Reflection
- JSON
- Text and HTML Templates
- Miscellaneous
- References
Golang Types
Data types are categorized into four categories as shown below.
Functions vs Methods
Function definition consists of func
keyword, function nam
e, parameters
and return values
if any exists.
Methods are functions that contain a receiver argument in their signature. Receiver arguments are used to access the properties of the receiver. The receiver can be a struct or non-struct type.
Concurrent programming
Golang enables two types of concurrent programming: goroutines
and channels
.
If goroutines
are the activities of a concurrent go
program, channels
are the connections between them.
goroutines
Each concurrently excuting activity is gcalled a goroutine
.
New goroutines are created by go
statement.
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
Other than by returning from main or exiting the program, there is no programmatic way for one goroutine
to stop another.
channels
A channel is a mechanism that lets one goroutine
send values to another goroutine
.
To create a channel, we use built-in make
function.
ch := make(chan int) // `ch` has type `chan int`
A channel is a reference to data structure created by make
. Two channels of the same type may be compared using ==. The comparison is true if both are references to the same channel data structure. A channel may also be compared to nil
.
channel
support three operations: send
, receive
and close
ch <- x // send statement
x = <-ch // receive expression in assignment statment
<-ch // receive statement, result is discarded
close(ch) // close operation
A send
statement transmits a value from one goroutine, through the channel, to another goroutine executing a corresponding receive
expression.
A close
operation sets a flag indicating no more values can be sent to this channel; subsequent attempts to send will panic. Receive operations on a closed channel yield the values that have been sent until no more values are left; any receive operations thereafter complete immediately and yield the zero value of the channel’s element type.
Unbuffered and Buffered Channels
A channel created with a simple call to make is called an unbuffered
channel, but make accepts an optional second argument, an integer called the channel’s capacity. If the capacity is non-zero, make creates a buffered
channel.
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
A send operation on an unbuffered channel blocks the sending goroutine
until another goroutine
executes a corresponding receive on the same channel, at which point the value is transmitted and both goroutines
may continue. Conversely, if the receive operation was attempted first, the receiving goroutine
is blocked until another goroutine performs a send on the same channel.
A buffered channel has a queue of elements. The queue’s maximum size is determined when it is created, by the capacity argument to make
.
Unidirectional Channel Types
To document channel usage intent and prevent misuse, the Go type system provides unidirectional
channel types that expose only one or the other of the send
and receive
operations.
chan<- int // send-only channel of int, allows sends only
<-chan int // receive-only channel of int, allows receives only
Violations of this discipline are detected at compile time.
Since the close
operation asserts that no more sends will occur on a channel, only the sending goroutine is in a position to call it, and for this reason it is a compile-time error to attempt to close a receive-only channel.
Multiplexing with select
General form of a select
statement is shown below. Like a switch
statement, it has a number of cases and an optional default. Each case specifies a communication (a send or receive operation on some channel) and an associated block of statements.
select {
case <-ch1:
// ...
case x := <-ch2:
// ...use x...
case ch3 <- y:
// ...
default:
// ...
}
A select waits until a communication for some case is ready to proceed. It then performs that communication and executes the case’s associated statements; the other communications do not happen. A select with no cases, select{}
, waits forever.
Cancellation
There is no way for one goroutine to terminate another directly, since that would leave all its shared variables in undefined states. But what if we need to cancel two goroutines, or an arbitrary number?
For cancellation, we need is a reliable mechanism to broadcast an event over a channel so that many goroutines can see it as it occurs and can later see that it has occurred.
As we know, after a channel has been closed and drained of all sent values, subsequent receive operations proceed immediately, yielding zero values. We can exploit this to create a broadcast mechanism: don’t send a value on the channel, close
it.
To to this, we create a cancellation channel on which no values are ever sent, but whose closure indicates that it is time for the program to stop what it is doing. All goroutines
which need to be communicated of an event does receive
on this channel. The goroutine
which need to broadcast event simply close
this channel.
Sync Package
The sync
package is an essential part of the Golang
concurrency implementation, which supports only two types, Mutex
and RWMutex
.
Mutual Exclusion: sync.Mutex
Mutex
is an implementation of an exclusive lock, based on which all the synchronization logic is built. It has 3 API functions: Lock
, Unlock
and TryLock
.
Lock
method acquires the token (called a lock) and its Unlock
method releases it. Each time a goroutine accesses shared variables, it must call the mutex’s Lock
method to acquire an exclusive lock. If some other goroutine has acquired the lock, this operation will block until the other goroutine calls Unlock
and the lock becomes available again. The mutex guards the shared variables.
TryLock
tries to take the lock and reports whether it succeeded.
Read/WriteMutexes: sync.RWMutex
RWMutex
allows read-only operations to proceed in parallel with each other (RLOCK
method), but write operations to have fully exclusive access (RUnlock
method). This lock is called a multiple readers, single writer lock. It is applicable for sync scenarios with more reads and fewer writes.
Lazy Initialization: sync.Once
It is good practice to defer an expensive initialization step until the moment it is needed. Initializing a variable up front increases the start-up latency of a program and is unnecessary if execution doesn’t always reach the par t of the program that uses that variable.
Once
has only one Do
method, and accepts only one func
parameter.
In the implementation, a Mutex
and uint
guarantees that Once
is executed only once, turning the unit from 0 to 1 after execution.
Example:
func main() {
var once sync.Once
onceBody := func() {
// some heavy operations
}
done := make(chan bool)
for i := 0; i < 10; i++ {
go func() {
once.Do(onceBody) // onceBody will only be called once.
done <- true
}()
}
for i := 0; i < 10; i++ {
<-done
}
}
Wait for Others: sync.WaitGroup
WaitGroup
allows one or more goroutines
to be blocked by a set of executing groutinues
. It provides three methods:
Add
adds delta sync-waiting goroutines.Done
reduces 1 from the counter after a goroutine finishes executing.Wait
implemented through an infinitefor
loop internally, exit upon the counter returns to 0, enabling the blocked goroutine to continue executing.
A typical WaitGroup
usage is as follows:
wg := &sync.WaitGroup{}
for i := 0; i < n; i++ { // executing goroutines for which main goroutine want to wait
wg.Add(1) // Add one for each goroutine
go func() {
// execute logic
wg.Done() // declare finished
}()
}
wg.Wait() // main goroutine is blocked till all goroutines finishes
Sync.Pool
Pool
purpose is to cache allocated but unused items for later reuse, relieving pressure on the garbage collector. That is, it makes it easy to build efficient, thread-safe free lists.
It can save memory overhead and reduce GC pressure in some cases.
Get
gets an object from thePool
with a key.Put
puts a key and an object intoPool
.
Example:
For scenarios with a large number of calls to the Log method, we obtain a Buffer from Pool and put it back to reduce the creation of Buffer.
var bufPool = sync.Pool{
New: func() any {
return new(bytes.Buffer)
},
}
func Log(w io.Writer, key, val string) {
b := bufPool.Get().(*bytes.Buffer)
b.Reset()
// Replace this with time.Now() in a real logger.
b.WriteString(timeNow().UTC().Format(time.RFC3339))
b.WriteByte(' ')
b.WriteString(key)
b.WriteByte('=')
b.WriteString(val)
w.Write(b.Bytes())
bufPool.Put(b)
}
Sync.Map
Map
is a Concurrent Map implementation. The two most common suitable scenarios are:
- When the entry for a given key is only ever written once but read many times, as in caches that only grow.
- When multiple goroutines read, write, and overwrite entries for disjoint sets of keys.
Generally, sync.Map
is more efficient than the synchronous map implemented by map
+ RWMutex
.
Type, Interface and Reflection
Understanding Type, Interface and Reflection should be done together because interface is a special type and reflection is built on types.
Golang is statically typed language. Every variable has a static type, that is, exactly one type known and fixed at compile time: int
, float32
, *UserType
, []byte
etc.
Interfaces
One important category of type is interface
types, which represent fixed sets of methods. Any type that implements these methods is said to implement the interface.
Interfaces are statically typed: a variable of interface type always has the same static type, and even though at run time the value stored in the interface variable may change type, that value will always satisfy the interface. An extremely important example of an interface type is the empty interface interface{}
which represents the empty set of methods and is satisfied by any value.
A variable of interface type stores a pair: the concrete value assigned to the variable, and that value’s type descriptor. To be more precise, the value is the underlying concrete data item that implements the interface and the type describes the full type of that item.
Let’s defined an interface shape
, which has two methods Area
and Perimeter
that accepts no arguments and return float64
value.
// Shape is an interface
type Shape interface {
Area() float64
Perimeter() float64
}
Since the interface
is a type just like a struct
, we can create a variable of its type. Let’s create a variable s
of type interface Shape
and print it’s type and value.
var s shape
fmt.Printf("Type of s is %T\n", s)
fmt.Printf("Value of s is %v\n", s)
It has zero value and nil type as variable s
is just declard of type Shape
but not assigned any value.
Type of s is <nil>
Value of s is <nil>
Any type that implements these methods (with exact method signatures) will also implement Shape
interface. Let’s define a new type Rectangle
which implements Shape
interface
// Rectangle is a type which implements `Shape` interface
type Rectangle struct {
length float64
breadth float64
}
// Area of Rectangle
func (r Rectangle) Area() float64 {
return r.length * r.breadth
}
// Perimeter of Rectangle
func (r Rectangle) Perimeter() float64 {
return 2 * (r.length + r.breadth)
}
Now, create of variable of type Rectangle
and assing it to s
and print type and value of s
.
var s Shape
s = Rectangle{1, 2}
fmt.Printf("Type of s is %T\n", s)
fmt.Printf("Value of s is %v\n", s)
We can see that, dynamic type of s
is now main.Rectangle
and dynamic value of s is the value of the struct Rect which is {1 2}
.
Type of s is main.Rectangle
Value of s is {1 2}
Two interface variables are equal if both of these variable holds the same dynamic type and dynamic value. Let’s define one more variable r
of Rectangle
type and having same value {1, 2}
var s Shape
s = Rectangle{1, 2}
r := Rectangle{1, 2}
fmt.Printf("Type of s is %T\n", s)
fmt.Printf("Value of s is %v\n", s)
fmt.Println("s == r is ", s == r)
They are equal as shown below
Type of s is main.Rectangle
Value of s is {1 2}
s == r is true
Reflection
interface value => reflection object
Reflection is a mechanism to examine the type and value pair stored inside an interface variable. The reflect package provides two types: Type and Value, which provide access to the content of an interface variable. Also, two functions: reflect.TypeOf
and reflect.ValueOf
, retrieve reflect.Type
and reflect.Value
pieces out of an interface value.
Important methods in reflect.Type
and reflect.Value
:
reflect.Value
has aType
method that returns theType
of areflect.Value
.- Both
reflect.Type
andreflect.Value
have aKind
method that returns a constant indicating what sort of item is stored.
reflection object => interface value
Like physical reflection, reflection in Go generates its own inverse.
Interface
method returns interface value from reflect.Value
. This method packs the type and value information back into an interface representation and returns the result:
// Interface returns v's value as an interface{}.
func (v Value) Interface() interface{}
The value must be settable t0 modify a reflection object
Settability is a property of a reflection Value
, and not all reflection Values
have it. It’s the property that a reflection object can modify the actual storage that was used to create the reflection object. Settability is determined by whether the reflection object holds the original item.
The CanSet
method of Value reports the settability of a Value
.
Using reflection to modify the fields of a structure
We create the reflection object with the address of the struct because we’ll want to modify it later. Then we set typeOfT
to its type and iterate over the fields using method calls. Note that we extract the names of the fields from the struct type, but the fields themselves are regular reflect.Value
objects.
type Rectangle struct {
Length int
Breadth float64
}
t := Rectangle{5, 6.6}
s := reflect.ValueOf(&t).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
f := s.Field(i)
fmt.Printf("%d: %s %s = %v\n", i,
typeOfT.Field(i).Name, f.Type(), f.Interface())
}
Output:
0: Length int = 5
1: Breadth float64 = 6.6
Note: The field names of Rectangle
are upper case (exported) because only exported fields of a struct are settable.
Because s
contains a settable reflection object, we can modify the fields of the structure.
s.Field(0).SetInt(77)
s.Field(1).SetFloat(99.8)
Output:
fmt.Println("t is now", t)
If we modified the program so that s
was created from t
, not &t
, the calls to SetInt
and SetFloat
would fail as the fields of t
would not be settable.
JSON
JSON is an encoding of JavaScript values—strings, numbers, booleans, arrays, and objects —as Unicode text. It’s an efficient yet readable representation for the basic data types of arrays, slices, structs, and maps.
Go has excellent support for encoding and decoding JavaScript Object Notation (JSON) format, provided by the standard library package encoding/json
.
- Converting a Go data structure to JSON is called
marshaling
. Marshaling is done byjson.Marshal
. Marshal produces a byte slice containing a very long string with no white space. For human consumption, a variant calledjson.MarshalIndent
produces neatly indented output. - The inverse operation to
marshaling
, decoding JSON and populating a Go data structure, is calledunmarshaling
, and it is done byjson.Unmarshal
. - Another useful function is
json.Indent
. It can be used to produced indented output from an existing JSON text. An example usage is shown below.
func main() {
input := []byte(`{"login": "sample-id","id": 12345678}`)
fmt.Println(input)
fmt.Println(string(prettify(input)))
}
func prettify(input []byte) []byte {
var prettyJSON bytes.Buffer
error := json.Indent(&prettyJSON, input, "", "\t")
if error != nil {
fmt.Println("JSON parse error: ", error)
return []byte{}
}
return prettyJSON.Bytes()
}
Text and HTML Templates
A template is a string or file containing one or more portions enclosed in double braces, {
{
…}
}
, called actions. Most of the string is printed literally, but each action contains an expression in the template language for
- Printing values
- Selecting struct fields
- Calling functions and methods
- Expressing control flow such as if-else statements
- Range loops
- Instantiating other templates
Good article explaing templates is at Gopher Academy Blog
Miscellaneous
Static vs Dynamic Linking
It is well known that Golang creates static binaries by default. I was surprised to find out all the Golang binaries that I was using were using dynamic linking. This was easily verified by using file command on binary
$ file test.gobin
file test.gobin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
To find out the libaries being linked, use ldd command
$ ldd test.gobin
linux-vdso.so.1 => (0x00007ffd4b7ed000)
libpthread.so.0 => /lib64/libpthread.so.0 (0x00000032d6e00000)
libc.so.6 => /lib64/libc.so.6 (0x00000032d6600000)
/lib64/ld-linux-x86-64.so.2 (0x0000560e965ba000)
The reason is the usage of CGO indirectly just by including the package “net”.
There are two packages which uses CGO.
- net
- os/user
Number literal prefixes
Go 1.13 allowed number literal prefixes, which allowed digit seperators among other things. The digits of any number literal may now be separated (grouped) using underscores, such as in 1_000_000
, 0b_1010_0110
, or 3.1415_9265
. An underscore may appear between any two digits or the literal prefix and the first digit.
References
- The Go Programming Language, Alan Donovan, B. Kernighan
- Gopher Academy Blog
- Go Data Structures: Interfaces
- The Go Blog