Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Master the fundamentals and advanced features of the Go programming language

License

NotificationsYou must be signed in to change notification settings

karanpratapsingh/learn-go

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

20 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hey, welcome to the course, and thanks for learning Go. I hope this course provides a great learning experience.

This course is also available on mywebsite and as an ebook onleanpub. Please leave a ⭐ as motivation if this was helpful!

Table of contents

What is Go?

Go (also known asGolang) is a programming language developed at Google in 2007 and open-sourced in 2009.

It focuses on simplicity, reliability, and efficiency. It was designed to combine the efficacy, speed, and safety of a statically typed and compiled language with the ease of programming of a dynamic language to make programming more fun again.

In a way, they wanted to combine the best parts of Python and C++ so that they can build reliable systems that can take advantage of multi-core processors.

Why learn Go?

Before we start this course, let us talk about why we should learn Go.

1. Easy to learn

Go is quite easy to learn and has a supportive and active community.

And being a multipurpose language you can use it for things like backend development, cloud computing, and more recently, data science.

2. Fast and Reliable

Which makes it highly suitable for distributed systems. Projects such as Kubernetes and Docker are written in Go.

3. Simple yet powerful

Go has just 25 keywords which makes it easy to read, write and maintain. The language itself is concise.

But don't be fooled by the simplicity, Go has several powerful features that we will later learn in the course.

4. Career opportunities

Go is growing fast and is being adopted by companies of any size. and with that, comes new high-paying job opportunities.

I hope this made you excited about Go. Let's start this course.

Installation and Setup

In this tutorial, we will install Go and setup our code editor.

Download

We can install Go from thedownloads section.

download

Installation

These instructions are from theofficial website.

MacOS

  1. Open the package file you downloaded and follow the prompts to install Go.The package installs the Go distribution to/usr/local/go. The package should put the/usr/local/go/bin directory in yourPATH environment variable.You may need to restart any open Terminal sessions for the change to take effect.

  2. Verify that you've installed Go by opening a command prompt and typing the following command:

$ go version
  1. Confirm that the command prints the installed version of Go.

Linux

  1. Remove any previous Go installation by deleting the/usr/local/go folder (if it exists),then extract the archive you just downloaded into/usr/local, creating a fresh Go tree in/usr/local/go:
$ rm -rf /usr/local/go && tar -C /usr/local -xzf go1.18.1.linux-amd64.tar.gz

Note: You may need to run the command as root or through sudo.

Do not untar the archive into an existing/usr/local/go tree. This is known to produce broken Go installations.

  1. Add/usr/local/go/bin to the PATH environment variable.You can do this by adding the following line to your$HOME/.profile or/etc/profile (for a system-wide installation):
export PATH=$PATH:/usr/local/go/bin

Note: Changes made to a profile file may not apply until the next time you log into your computer. To apply the changes immediately, just run the shell commands directly or execute them from the profile using a command such as source$HOME/.profile.

  1. Verify that you've installed Go by opening a command prompt and typing the following command:
$ go version
  1. Confirm that the command prints the installed version of Go.

Windows

  1. Open the MSI file you downloaded and follow the prompts to install Go.

By default, the installer will install Go to Program Files or Program Files (x86).You can change the location as needed. After installing, you will need to close and reopen any open command prompts so that changes to the environment made by the installer are reflected at the command prompt.

  1. Verify that you've installed Go.
    1. In Windows, click the Start menu.
    2. In the menu's search box, type cmd, then press the Enter key.
    3. In the Command Prompt window that appears, type the following command:
$ go version
  1. Confirm that the command prints the installed version of Go.

VS Code

In this course, I will be usingVS Code and you can download it fromhere.

vscode

Feel free to use any other code editor you prefer.

Extension

Make sure to also install theGo extension which makes it easier to work with Go in VS Code.

extension

This is it for the installation and setup of Go, let's start the course and write our first hello world!

Hello World

Let's write our first hello world program, we can start by initializing a module. For that, we can use thego mod command.

$ go mod init example

But wait...what's amodule? Don't worry we will discuss that soon! But for now, assume that the module is basically a collection of Go packages.

Moving ahead, let's now create amain.go file and write a program that simply prints hello world.

package mainimport"fmt"funcmain() {fmt.Println("Hello World!")}

If you're wondering,fmt is part of the Go standard library which is a set of core packages provided by the language.

Structure of a Go program

Now, let's quickly break down what we did here, or rather the structure of a Go program.

First, we defined a package such asmain.

package main

Then, we have some imports.

import"fmt"

Last but not least, is ourmain function which acts as an entry point for our application, just like in other languages like C, Java, or C#.

funcmain() {...}

Remember, the goal here is to keep a mental note, and later in the course, we'll learn aboutfunctions,imports, and other things in detail!

Finally, to run our code, we can simply usego run command.

$ go run main.goHello World!

Congratulations, you just wrote your first Go program!

Variables and Data Types

In this tutorial, we will learn about variables. We will also learn about the different data types that Go provides us.

Variables

Let's start with declaring a variable.

This is also known as declaration without initialization:

varfoostring

Declaration with initialization:

varfoostring="Go is awesome"

Multiple declarations:

varfoo,barstring="Hello","World"// ORvar (foostring="Hello"barstring="World")

Type is omitted but will be inferred:

varfoo="What's my type?"

Shorthand declaration, here we omitvar keyword and type is always implicit. This is how we will see variables being declared most of the time. We also use the:= for declaration plus assignment.

foo:="Shorthand!"

Note: Shorthand only works insidefunction bodies.

Constants

We can also declare constants with theconst keyword. Which as the name suggests, are fixed values that cannot be reassigned.

constconstant="This is a constant"

It is also important to note that, only constants can be assigned to other constants.

consta=10constb=a// ✅ Worksvara=10constb=a// ❌ a (variable of type int) is not constant (InvalidConstInit)

Data Types

Perfect! Now let's look at some basic data types available in Go. Starting with string.

String

In Go, a string is a sequence of bytes. They are declared either using double quotes or backticks which can span multiple lines.

varnamestring="My name is Go"varbiostring=`I am statically typed.I was designed at Google.`

Bool

Next isbool which is used to store boolean values. It can have two possible values -true orfalse.

varvaluebool=falsevarisItTruebool=true

Operators

We can use the following operators on boolean types

TypeSyntax
Logical&&||!
Equality==!=

Numeric types

Now, let's talk about numeric types.

Signed and Unsigned integers

Go has several built-in integer types of varying sizes for storing signed and unsigned integers

The size of the genericint anduint types are platform-dependent. This means it is 32-bits wide on a 32-bit system and 64-bits wide on a 64-bit system.

variint=404// Platform dependentvari8int8=127// -128 to 127vari16int16=32767// -2^15 to 2^15 - 1vari32int32=-2147483647// -2^31 to 2^31 - 1vari64int64=9223372036854775807// -2^63 to 2^63 - 1

Similar to signed integers, we have unsigned integers.

varuiuint=404// Platform dependentvarui8uint8=255// 0 to 255varui16uint16=65535// 0 to 2^16varui32uint32=2147483647// 0 to 2^32varui64uint64=9223372036854775807// 0 to 2^64varuiptruintptr// Integer representation of a memory address

If you noticed, there's also an unsigned integer pointeruintptr type, which is an integer representation of a memory address. It is not recommended to use this, so we don't have to worry about it.

So which one should we use?

It is recommended that whenever we need an integer value, we should just useint unless we have a specific reason to use a sized or unsigned integer type.

Byte and Rune

Golang has two additional integer types calledbyte andrune that are aliases foruint8 andint32 data types respectively.

typebyte=uint8typerune=int32

Arune represents a unicode code point.

varbbyte='a'varrrune='🍕'

Floating point

Next, we have floating point types which are used to store numbers with a decimal component.

Go has two floating point typesfloat32 andfloat64. Both type follows the IEEE-754 standard.

The default type for floating point values is float64.

varf32float32=1.7812// IEEE-754 32-bitvarf64float64=3.1415// IEEE-754 64-bit

Operators

Go provides several operators for performing operations on numeric types.

TypeSyntax
Arithmetic+-*/%
Comparison==!=<><=>=
Bitwise&|^<<>>
Increment/Decrement++--
Assignment=+=-=*=/=%=<<=>>=&=|=^=

Complex

There are 2 complex types in Go,complex128 where both real and imaginary parts arefloat64 andcomplex64 where real and imaginary parts arefloat32.

We can define complex numbers either using the built-in complex function or as literals.

varc1complex128=complex(10,1)varc2complex64=12+4i

Zero Values

Now let's discuss zero values. So in Go, any variable declared without an explicit initial value is given itszero value. For example, let's declare some variables and see:

variintvarffloat64varbboolvarsstringfmt.Printf("%v %v %v %q\n",i,f,b,s)
$ go run main.go0 0false""

So, as we can seeint andfloat are assigned as 0,bool as false, andstring as an empty string. This is quite different from how other languages do it. For example, most languages initialize unassigned variables as null or undefined.

This is great, but what are those percent symbols in ourPrintf function? As you've already guessed, they are used for formatting and we will learn about them later.

Type Conversion

Moving on, now that we have seen how data types work, let's see how to do type conversion.

i:=42f:=float64(i)u:=uint(f)fmt.Printf("%T %T",f,u)
$ go run main.gofloat64 uint

And as we can see, it prints the type asfloat64 anduint.

Note that this is different from parsing.

Alias types

Alias types were introduced in Go 1.9. They allow developers to provide an alternate name for an existing type and use it interchangeably with the underlying type.

package mainimport"fmt"typeMyAlias=stringfuncmain() {varstrMyAlias="I am an alias"fmt.Printf("%T - %s",str,str)// Output: string - I am an alias}

Defined types

Lastly, we have defined types that unlike alias types do not use an equals sign.

package mainimport"fmt"typeMyDefinedstringfuncmain() {varstrMyDefined="I am defined"fmt.Printf("%T - %s",str,str)// Output: main.MyDefined - I am defined}

But wait...what's the difference?

So, defined types do more than just give a name to a type.

It first defines a new named type with an underlying type. However, this defined type is different from any other type, including its underline type.

Hence, it cannot be used interchangeably with the underlying type like alias types.

It's a bit confusing at first, hopefully, this example will make things clear.

package mainimport"fmt"typeMyAlias=stringtypeMyDefinedstringfuncmain() {varaliasMyAliasvardefMyDefined// ✅ Worksvarcopy1string=alias// ❌ Cannot use def (variable of type MyDefined) as string value in variablevarcopy2string=deffmt.Println(copy1,copy2)}

As we can see, we cannot use the defined type interchangeably with the underlying type, unlikealias types.

String Formatting

In this tutorial, we will learn about string formatting or sometimes also known as templating.

fmt package contains lots of functions. So to save time, we will discuss the most frequently used functions. Let's start withfmt.Print inside our main function.

...fmt.Print("What","is","your","name?")fmt.Print("My","name","is","golang")...
$ go run main.goWhatisyourname?Mynameisgolang

As we can see,Print does not format anything, it simply takes a string and prints it.

Next, we havePrintln which is the same asPrint but it adds a new line at the end and also inserts space between the arguments.

...fmt.Println("What","is","your","name?")fmt.Println("My","name","is","golang")...
$ go run main.goWhat is your name?My name is golang

That's much better!

Next, we havePrintf also known as"Print Formatter", which allows us to format numbers, strings, booleans, and much more.

Let's look at an example.

...name:="golang"fmt.Println("What is your name?")fmt.Printf("My name is %s",name)...
$ go run main.goWhat is your name?My name is golang

As we can see that%s was substituted with ourname variable.

But the question is what is%s and what does it mean?

So, these are calledannotation verbs and they tell the function how to format the arguments. We can control things like width, types, and precision with these and there are lots of them. Here's acheatsheet.

Now, let's quickly look at some more examples. Here we will try to calculate a percentage and print it to the console.

...percent:= (7.0/9)*100fmt.Printf("%f",percent)...
$ go run main.go77.777778

Let's say we want just77.78 which is 2 points precision, we can do that as well by using.2f.

Also, to add an actual percent sign, we will need toescape it.

...percent:= (7.0/9)*100fmt.Printf("%.2f %%",percent)...
$ go run main.go77.78 %

This brings us toSprint,Sprintln, andSprintf. These are basically the same as the print functions, the only difference being they return the string instead of printing it.

Let's take a look at an example.

...s:=fmt.Sprintf("hex:%x bin:%b",10 ,10)fmt.Println(s)...
$ go run main.gohex:a bin:1010

So, as we can seeSprintf formats our integer as hex or binary and returns it as a string.

Lastly, we have multiline string literals, which can be used like this.

...msg:=`Hello frommultiline`fmt.Println(msg)...

Great! But this is just the tip of the iceberg...so make sure to check out the go doc forfmt package.

For those who are coming from C/C++ background, this should feel natural, but if you're coming from, let's say Python or JavaScript, this might be a little strange at first. But it is very powerful and you'll see this functionality used quite extensively.

Flow Control

Let's talk about flow control, starting with if/else.

If/Else

This works pretty much the same as you expect but the expression doesn't need to be surrounded by parentheses().

funcmain() {x:=10ifx>5 {fmt.Println("x is gt 5")}elseifx>10 {fmt.Println("x is gt 10")}else {fmt.Println("else case")}}
$ go run main.gox is gt 5

Compact if

We can also compact our if statements.

funcmain() {ifx:=10;x>5 {fmt.Println("x is gt 5")}}

Note: This pattern is quite common.

Switch

Next, we haveswitch statement, which is often a shorter way to write conditional logic.

In Go, the switch case only runs the first case whose value is equal to the condition expression and not all the cases that follow. Hence, unlike other languages,break statement is automatically added at the end of each case.

This means that it evaluates cases from top to bottom, stopping when a case succeeds.Let's take a look at an example:

funcmain() {day:="monday"switchday {case"monday":fmt.Println("time to work!")case"friday":fmt.Println("let's party")default:fmt.Println("browse memes")}}
$ go run main.gotime to work!

Switch also supports shorthand declaration like this.

switchday:="monday";day {case"monday":fmt.Println("time to work!")case"friday":fmt.Println("let's party")default:fmt.Println("browse memes")}

We can also use thefallthrough keyword to transfer control to the next case even though the current case might have matched.

switchday:="monday";day {case"monday":fmt.Println("time to work!")fallthroughcase"friday":fmt.Println("let's party")default:fmt.Println("browse memes")}

And if we run this, we'll see that after the first case matches the switch statement continues to the next case because of thefallthrough keyword.

$ go run main.gotime to work!let's party

We can also use it without any condition, which is the same asswitch true.

x:=10switch {casex>5:fmt.Println("x is greater")default:fmt.Println("x is not greater")}

Loops

Now, let's turn our attention toward loops.

So in Go, we only have one type of loop which is thefor loop.

But it's incredibly versatile. Same as if statement, for loop, doesn't need any parenthesis() unlike other languages.

For loop

Let's start with the basicfor loop.

funcmain() {fori:=0;i<10;i++ {fmt.Println(i)}}

The basicfor loop has three components separated by semicolons:

  • init statement: which is executed before the first iteration.
  • condition expression: which is evaluated before every iteration.
  • post statement: which is executed at the end of every iteration.

Break and continue

As expected, Go also supports bothbreak andcontinue statements for loop control. Let's try a quick example:

funcmain() {fori:=0;i<10;i++ {ifi<2 {continue}fmt.Println(i)ifi>5 {break}}fmt.Println("We broke out!")}

So, thecontinue statement is used when we want to skip the remaining portion of the loop, andbreak statement is used when we want to break out of the loop.

Also, Init and post statements are optional, hence we can make ourfor loop behave like a while loop as well.

funcmain() {i:=0for ;i<10; {i+=1}}

Note: we can also remove the additional semi-colons to make it a little cleaner.

Forever loop

Lastly, If we omit the loop condition, it loops forever, so an infinite loop can be compactly expressed. This is also known as the forever loop.

funcmain() {for {// do stuff here}}

Functions

In this tutorial, we will discuss how we work with functions in Go. So, let's start with a simple function declaration.

Simple declaration

funcmyFunction() {}

And we cancall or execute it as follows.

...myFunction()...

Let's pass some parameters to it.

funcmain() {myFunction("Hello")}funcmyFunction(p1string) {fmt.Println(p1)}
$ go run main.go

As we can see it prints our message. We can also do a shorthand declaration if the consecutive parameters have the same type. For example:

funcmyNextFunction(p1,p2string) {}

Returning the value

Now let's also return a value.

funcmain() {s:=myFunction("Hello")fmt.Println(s)}funcmyFunction(p1string)string {msg:=fmt.Sprintf("%s function",p1)returnmsg}

Multiple returns

Why return one value at a time, when we can do more? Go also supports multiple returns!

funcmain() {s,i:=myFunction("Hello")fmt.Println(s,i)}funcmyFunction(p1string) (string,int) {msg:=fmt.Sprintf("%s function",p1)returnmsg,10}

Named returns

Another cool feature isnamed returns, where return values can be named and treated as their own variables.

funcmyFunction(p1string) (sstring,iint) {s=fmt.Sprintf("%s function",p1)i=10return}

Notice how we added areturn statement without any arguments, this is also known asnaked return.

I will say that, although this feature is interesting, please use it with care as this might reduce readability for larger functions.

Functions as values

Next, let's talk about functions as values, in Go functions are first class and we can use them as values. So, let's clean up our function and try it out!

funcmyFunction() {fn:=func() {fmt.Println("inside fn")}fn()}

We can also simplify this by makingfn ananonymous function.

funcmyFunction() {func() {fmt.Println("inside fn")}()}

Notice how we execute it using the parenthesis at the end.

Closures

Why stop there? let's also return a function and hence create something called a closure. A simple definition can be that a closure is a function value that references variables from outside its body.

Closures are lexically scoped, which means functions can access the values in scope when defining the function.

funcmyFunction()func(int)int {sum:=0returnfunc(vint)int {sum+=vreturnsum}}
...add:=myFunction()add(5)fmt.Println(add(10))...

As we can see, we get a result of 15 assum variable isbound to the function. This is a very powerful concept and definitely, a must know.

Variadic Functions

Now let's look at variadic functions, which are functions that can take zero or multiple arguments using the... ellipses operator.

An example here would be a function that can add a bunch of values.

funcmain() {sum:=add(1,2,3,5)fmt.Println(sum)}funcadd(values...int)int {sum:=0for_,v:=rangevalues {sum+=v}returnsum}

Pretty cool huh? Also, don't worry about therange keyword, we will discuss it later in the course.

Fun fact:fmt.Println is a variadic function, that's how we were able to pass multiple values to it.

Init

In Go,init is a special lifecycle function that is executed before themain function.

Similar tomain, theinit function does not take any arguments nor returns any value. Let's see how it works with an example.

package mainimport"fmt"funcinit() {fmt.Println("Before main!")}funcmain() {fmt.Println("Running main")}

As expected, theinit function was executed before themain function.

$ go run main.goBefore main!Running main

Unlikemain, there can be more than oneinit function in single or multiple files.

For multipleinit in a single file, their processing is done in the order of their declaration, whileinit functions declared in multiple files are processed according to the lexicographic filename order.

package mainimport"fmt"funcinit() {fmt.Println("Before main!")}funcinit() {fmt.Println("Hello again?")}funcmain() {fmt.Println("Running main")}

And if we run this, we'll see theinit functions were executed in the order they were declared.

$ go run main.goBefore main!Hello again?Running main

Theinit function is optional and is particularly used for any global setup which might be essential for our program, such as establishing a database connection, fetching configuration files, setting up environment variables, etc.

Defer

Lastly, let's discuss thedefer keyword, which lets us postpones the execution of a function until the surrounding function returns.

funcmain() {deferfmt.Println("I am finished")fmt.Println("Doing some work...")}

Can we use multiple defer functions? Absolutely, this brings us to what is known asdefer stack. Let's take a look at an example:

funcmain() {deferfmt.Println("I am finished")deferfmt.Println("Are you?")fmt.Println("Doing some work...")}
$ go run main.goDoing some work...Are you?I am finished

As we can see, defer statements are stacked and executed in alast in first out manner.

So, Defer is incredibly useful and is commonly used for doing cleanup or error handling.

Functions can also be used with generics but we will discuss them later in the course.

Modules

In this tutorial, we will learn about modules.

What are modules?

Simply defined, A module is a collection ofGo packages stored in a file tree with ago.mod file at its root, provided the directory isoutside$GOPATH/src.

Go modules were introduced in Go 1.11, which brings native support for versions and modules. Earlier, we needed theGO111MODULE=on flag to turn on the modules functionality when it was experimental. But now after Go 1.13 modules mode is the default for all development.

But wait, what isGOPATH?

Well,GOPATH is a variable that defines the root of your workspace and it contains the following folders:

  • src: contains Go source code organized in a hierarchy.
  • pkg: contains compiled package code.
  • bin: contains compiled binaries and executables.

gopath

Like earlier, let's create a new module usinggo mod init command which creates a new module and initializes thego.mod file that describes it.

$ go mod init example

The important thing to note here is that a Go module can correspond to a Github repository as well if you plan to publish this module. For example:

$ go mod init example

Now, let's explorego.mod which is the file that defines the module'smodule path and also the import path used for the root directory, and itsdependency requirements.

module<name>go<version>require (...)

And if we want to add a new dependency, we will usego install command:

$ go install github.com/rs/zerolog

As we can see ago.sum file was also created. This file contains the expectedhashes of the content of the new modules.

We can list all the dependencies usinggo list command as follows:

$ go list -m all

If the dependency is not used, we can simply remove it usinggo mod tidy command:

$ go mod tidy

Finishing up our discussion on modules, let's also discuss vendoring.

Vendoring is the act of making your own copy of the 3rd party packages your project is using. Those copies are traditionally placed inside each project and then saved in the project repository.

This can be done throughgo mod vendor command.

So, let's reinstall the removed module usinggo mod tidy.

package mainimport"github.com/rs/zerolog/log"funcmain() {log.Info().Msg("Hello")}
$ go mod tidygo: finding modulefor package github.com/rs/zerolog/loggo: found github.com/rs/zerolog/login github.com/rs/zerolog v1.26.1
$ go mod vendor

After thego mod vendor command is executed, avendor directory will be created.

├── go.mod├── go.sum├── go.work├── main.go└── vendor    ├── github.com    │   └── rs    │       └── zerolog    │           └── ...    └── modules.txt

Packages

In this tutorial, we will talk about packages.

What are packages?

A package is nothing but a directory containing one or more Go source files, or other Go packages.

This means every Go source file must belong to a package and package declaration is done at top of every source file as follows.

package<package_name>

So far, we've done everything inside ofpackage main. By convention, executable programs (by that I mean the ones with themain package) are calledCommands, others are simply calledPackages.

Themain package should also contain amain() function which is a special function that acts as the entry point of an executable program.

Let's take a look at an example by creating our own packagecustom and adding some source files to it such ascode.go.

package custom

Before we proceed any further, we should talk about imports and exports. Just like other languages, go also has a concept of imports and exports but it's very elegant.

Basically, any value (like a variable or function) can be exported and visible from other packages if they have been defined with an upper case identifier.

Let's try an example in ourcustom package.

package customvarvalueint=10// Will not be exportedvarValueint=20// Will be exported

As we can see lower case identifiers will not be exported and will be private to the package it's defined in. In our case thecustom package.

That's great but how do we import or access it? Well, same as we've been doing so far unknowingly. Let's go to ourmain.go file and import ourcustom package.

Here we can refer to it using themodule we had initialized in ourgo.mod file earlier.

---go.mod---moduleexamplego1.18---main.go--package mainimport"example/custom"funcmain() {custom.Value}

Notice how the package name is the last name of the import path.

We can import multiple packages as well like this.

package mainimport ("fmt""example/custom")funcmain() {fmt.Println(custom.Value)}

We can also alias our imports to avoid collisions like this.

package mainimport ("fmt"abcd"example/custom")funcmain() {fmt.Println(abcd.Value)}

External Dependencies

In Go, we are not only limited to working with local packages, we can also install external packages usinggo install command as we saw earlier.

So let's download a simple logging packagegithub.com/rs/zerolog/log.

$ go install github.com/rs/zerolog
package mainimport ("github.com/rs/zerolog/log"abcd"example/custom")funcmain() {log.Print(abcd.Value)}

Also, make sure to check out the go doc of packages you install, which is usually located in the project's readme file. go doc parses the source code and generates documentation in HTML format. Reference to It is usually located in readme files.

Lastly, I will add that, Go doesn't have a particular"folder structure" convention, always try to organize your packages in a simple and intuitive way.

Workspaces

In this tutorial, we will learn about multi-module workspaces that were introduced in Go 1.18.

Workspaces allow us to work with multiple modules simultaneously without having to editgo.mod files for each module. Each module within a workspace is treated as a root module when resolving dependencies.

To understand this better, let's start by creating ahello module.

$ mkdir workspaces&&cd workspaces$ mkdir hello&&cd hello$ go mod init hello

For demonstration purposes, I will add a simplemain.go and install an example package.

package mainimport ("fmt""golang.org/x/example/stringutil")funcmain() {result:=stringutil.Reverse("Hello Workspace")fmt.Println(result)}
$ go get golang.org/x/examplego: downloading golang.org/x/example v0.0.0-20220412213650-2e68773dfca0go: added golang.org/x/example v0.0.0-20220412213650-2e68773dfca0

And if we run this, we should see our output in reverse.

$ go run main.goecapskroW olleH

This is great, but what if we want to modify thestringutil module that our code depends on?

Until now, we had to do it using thereplace directive in thego.mod file, but now let's see how we can use workspaces here.

So, let's create our workspace in theworkspaces directory.

$ go work init

This will create ago.work file.

$ cat go.workgo 1.18

We will also add ourhello module to the workspace.

$ go work use ./hello

This should update thego.work file with a reference to ourhello module.

go1.18use ./hello

Now, let's download and modify thestringutil package and update theReverse function implementation.

$ git clone https://go.googlesource.com/exampleCloning into'example'...remote: Total 204 (delta 39), reused 204 (delta 39)Receiving objects: 100% (204/204), 467.53 KiB| 363.00 KiB/s, done.Resolving deltas: 100% (39/39), done.

example/stringutil/reverse.go

funcReverse(sstring)string {returnfmt.Sprintf("I can do whatever!! %s",s)}

Finally, let's addexample package to our workspace.

$ go work use ./example$ cat go.workgo 1.18use (./example./hello)

Perfect, now if we run ourhello module we will notice that theReverse function has been modified.

$ go run helloI cando whatever!! Hello Workspace

This is a very underrated feature from Go 1.18 but it is quite useful in certain circumstances.

Useful Commands

During our module discussion, we discussed some go commands related to go modules, let's now discuss some other important commands.

Starting withgo fmt, which formats the source code and it's enforced by that language so that we can focus on how our code should work rather than how our code should look.

$ go fmt

This might seem a little weird at first especially if you're coming from a javascript or python background like me but frankly, it's quite nice not to worry about linting rules.

Next, we havego vet which reports likely mistakes in our packages.

So, if I go ahead and make a mistake in the syntax, and then rungo vet.

It should notify me of the errors.

$ go vet

Next, we havego env which simply prints all the go environment information, we'll learn about some of these build-time variables later.

Lastly, we havego doc which shows documentation for a package or symbol, here's an example of thefmt package.

$ go doc -src fmt Printf

Let's usego help command to see what other commands are available.

$ gohelp

As we can see, we have:

go fix finds Go programs that use old APIs and rewrites them to use newer ones.

go generate is usually used for code generation.

go install compiles and installs packages and dependencies.

go clean is used for cleaning files that are generated by compilers.

Some other very important commands arego build andgo test but we will learn about them in detail later in the course.

Build

Building static binaries is one of the best features of Go which enables us to ship our code efficiently.

We can do this very easily using thego build command.

package mainimport"fmt"funcmain() {fmt.Println("I am a binary!")}
$ go build

This should produce a binary with the name of our module. For example, here we haveexample.

We can also specify the output.

$ go build -o app

Now to run this, we simply need to execute it.

$ ./appI am a binary!

Yes, it's as simple as that!

Now, let's talk about some important build time variables, starting with:

  • GOOS andGOARCH

These environment variables help us build go programs for differentoperating systemsand underlying processorarchitectures.

We can list all the supported architecture usinggo tool command.

$ go tool dist listandroid/amd64ios/amd64js/wasmlinux/amd64windows/arm64...

Here's an example for building a windows executable from macOS!

$ GOOS=windows GOARCH=amd64 go build -o app.exe
  • CGO_ENABLED

This variable allows us to configureCGO, which is a way in Go to call C code.

This helps us to produce astatically linked binary that works without any external dependencies.

This is quite helpful for, let's say when we want to run our go binaries in a docker container with minimum external dependencies.

Here's an example of how to use it:

$ CGO_ENABLED=0 go build -o app

Pointers

In this tutorial, we will discuss pointers. So what are Pointers?

Simply defined, a Pointer is a variable that is used to store the memory address of another variable.

pointers

It can be used like this:

varx*T

WhereT is the type such asint,string,float, and so on.

Let's try a simple example and see it in action.

package mainimport"fmt"funcmain() {varp*intfmt.Println(p)}
$ go run main.gonil

Hmm, this printsnil, but what isnil?

So nil is a predeclared identifier in Go that represents zero value for pointers, interfaces, channels, maps, and slices.

This is just like what we learned in the variables and datatypes section, where we saw that uninitializedint has a zero value of 0, abool has false, and so on.

Okay, now let's assign a value to the pointer.

package mainimport"fmt"funcmain() {a:=10varp*int=&afmt.Println("address:",p)}

We use the& ampersand operator to refer to a variable's memory address.

$ go run main.go0xc0000b8000

This must be the value of the memory address of the variablea.

Dereferencing

We can also use the* asterisk operator to retrieve the value stored in the variable that the pointer points to. This is also calleddereferencing.

For example, we can access the value of the variablea through the pointerp using that* asterisk operator.

package mainimport"fmt"funcmain() {a:=10varp*int=&afmt.Println("address:",p)fmt.Println("value:",*p)}
$ go run main.goaddress: 0xc000018030value: 10

We can not only access it but change it as well through the pointer.

package mainimport"fmt"funcmain() {a:=10varp*int=&afmt.Println("before",a)fmt.Println("address:",p)*p=20fmt.Println("after:",a)}
$ go run main.gobefore 10address: 0xc000192000after: 20

I think this is pretty neat!

Pointers as function args

Pointers can also be used as arguments for a function when we need to pass some data by reference.

Here's an example:

myFunction(&a)...funcmyFunction(ptr*int) {}

New function

There's also another way to initialize a pointer. We can use thenew function which takes a type as an argument, allocates enough memory to accommodate a value of that type, and returns a pointer to it.

Here's an example:

package mainimport"fmt"funcmain() {p:=new(int)*p=100fmt.Println("value",*p)fmt.Println("address",p)}
$ go run main.govalue 100address 0xc000018030

Pointer to a Pointer

Here's an interesting idea...can we create a pointer to a pointer? The answer is yes! Yes, we can.

package mainimport"fmt"funcmain() {p:=new(int)*p=100p1:=&pfmt.Println("P value",*p," address",p)fmt.Println("P1 value",*p1," address",p)fmt.Println("Dereferenced value",**p1)}
$ go run main.goP value 100  address 0xc0000be000P1 value 0xc0000be000  address 0xc0000be000Dereferenced value 100

Notice how the value ofp1 matches the address ofp.

Also, it is important to know that pointers in Go do not support pointer arithmetic like in C or C++.

p1:=p*2// Compiler Error: invalid operation

However, we can compare two pointers of the same type for equality using a== operator.

p:=&ap1:=&afmt.Println(p==p1)

But Why?

This brings us to the million-dollar question, why do we need pointers?

Well, there's no definite answer for that, and pointers are just another useful feature that helps us mutate our data efficiently without copying a large amount of data.

Lastly, I will add that if you are coming from a language with no notion of pointers, don't panic and try to form a mental model of how pointers work.

Structs

In this tutorial, we will learn about structs.

So, astruct is a user-defined type that contains a collection of named fields. Basically, it is used to group related data together to form a single unit.

If you're coming from an objected-oriented background, think of structs as lightweight classes which that support composition but not inheritance.

Defining

We can define astruct like this:

typePersonstruct {}

We use thetype keyword to introduce a new type, followed by the name and then thestruct keyword to indicate that we're defining a struct.

Now, let's give it some fields:

typePersonstruct {FirstNamestringLastNamestringAgeint}

And, if the fields have the same type, we can collapse them as well.

typePersonstruct {FirstName,LastNamestringAgeint}

Declaring and initializing

Now that we have our struct, we can declare it the same as other datatypes.

funcmain() {varp1Personfmt.Println("Person 1:",p1)}
$ go run main.goPerson 1: {  0}

As we can see, all the struct fields are initialized with their zero values. So theFirstName andLastName are set to"" empty string andAge is set to 0.

We can also initialize it as"struct literal".

funcmain() {varp1Personfmt.Println("Person 1:",p1)varp2=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22}fmt.Println("Person 2:",p2)}

For readability, we can separate by new line but this will also require a trailing comma.

varp2=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}
$ go run main.goPerson 1: {  0}Person 2: {Karan Pratap Singh 22}

We can also initialize only a subset of fields.

funcmain() {varp1Personfmt.Println("Person 1:",p1)varp2=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}fmt.Println("Person 2:",p2)varp3=Person{FirstName:"Tony",LastName:"Stark",}fmt.Println("Person 3:",p3)}
$ go run main.goPerson 1: {  0}Person 2: {Karan Pratap Singh 22}Person 3: {Tony Stark 0}

As we can see, the age field of person 3 has defaulted to the zero value.

Without field name

Go structs also supports initialization without field names.

funcmain() {varp1Personfmt.Println("Person 1:",p1)varp2=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}fmt.Println("Person 2:",p2)varp3=Person{FirstName:"Tony",LastName:"Stark",}fmt.Println("Person 3:",p3)varp4=Person{"Bruce","Wayne"}fmt.Println("Person 4:",p4)}

But here's the catch, we will need to provide all the values during the initialization or it will fail.

$ go run main.go# command-line-arguments./main.go:30:27: too few valuesin Person{...}
varp4=Person{"Bruce","Wayne",40}fmt.Println("Person 4:",p4)

We can also declare an anonymous struct.

funcmain() {varp1Personfmt.Println("Person 1:",p1)varp2=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}fmt.Println("Person 2:",p2)varp3=Person{FirstName:"Tony",LastName:"Stark",}fmt.Println("Person 3:",p3)varp4=Person{"Bruce","Wayne",40}fmt.Println("Person 4:",p4)vara=struct {Namestring}{"Golang"}fmt.Println("Anonymous:",a)}

Accessing fields

Let's clean up our example a bit and see how we can access individual fields.

funcmain() {varp=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}fmt.Println("FirstName",p.FirstName)}

We can also create a pointer to structs as well.

funcmain() {varp=Person{FirstName:"Karan",LastName:"Pratap Singh",Age:22,}ptr:=&pfmt.Println((*ptr).FirstName)fmt.Println(ptr.FirstName)}

Both statements are equal as in Go we don't need to explicitly dereference the pointer. We can also use the built-innew function.

funcmain() {p:=new(Person)p.FirstName="Karan"p.LastName="Pratap Singh"p.Age=22fmt.Println("Person",p)}
$ go run main.goPerson&{Karan Pratap Singh 22}

As a side note, two structs are equal if all their corresponding fields are equal as well.

funcmain() {varp1=Person{"a","b",20}varp2=Person{"a","b",20}fmt.Println(p1==p2)}
$ go run main.gotrue

Exported fields

Now let's learn what is exported and unexported fields in a struct. Same as the rules for variables and functions, if a struct field is declared with a lower case identifier, it will not be exported and only be visible to the package it is defined in.

typePersonstruct {FirstName,LastNamestringAgeintzipCodestring}

So, thezipCode field won't be exported. Also, the same goes for thePerson struct, if we rename it asperson, it won't be exported as well.

typepersonstruct {FirstName,LastNamestringAgeintzipCodestring}

Embedding and composition

As we discussed earlier, Go doesn't necessarily support inheritance, but we can do something similar with embedding.

typePersonstruct {FirstName,LastNamestringAgeint}typeSuperHerostruct {PersonAliasstring}

So, our new struct will have all the properties of the original struct. And it should behave the same as our normal struct.

funcmain() {s:=SuperHero{}s.FirstName="Bruce"s.LastName="Wayne"s.Age=40s.Alias="batman"fmt.Println(s)}
$ go run main.go{{Bruce Wayne 40} batman}

However, this is usually not recommended and in most cases, composition is preferred. So rather than embedding, we will just define it as a normal field.

typePersonstruct {FirstName,LastNamestringAgeint}typeSuperHerostruct {PersonPersonAliasstring}

Hence, we can rewrite our example with composition as well.

funcmain() {p:=Person{"Bruce","Wayne",40}s:=SuperHero{p,"batman"}fmt.Println(s)}
$ go run main.go{{Bruce Wayne 40} batman}

Again, there is no right or wrong here, but nonetheless, embedding comes in handy sometimes.

Struct tags

A struct tag is just a tag that allows us to attach metadata information to the field which can be used for custom behavior using thereflect package.

Let's learn how we can define struct tags.

typeAnimalstruct {Namestring`key:"value1"`Ageint`key:"value2"`}

You will often find tags in encoding packages, such as XML, JSON, YAML, ORMs, and Configuration management.

Here's a tags example for the JSON encoder.

typeAnimalstruct {Namestring`json:"name"`Ageint`json:"age"`}

Properties

Finally, let's discuss the properties of structs.

Structs are value types. When we assign onestruct variable to another, a new copy of thestruct is created and assigned.

Similarly, when we pass astruct to another function, the function gets its own copy of thestruct.

package mainimport"fmt"typePointstruct {X,Yfloat64}funcmain() {p1:=Point{1,2}p2:=p1// Copy of p1 is assigned to p2p2.X=2fmt.Println(p1)// Output: {1 2}fmt.Println(p2)// Output: {2 2}}

Empty struct occupies zero bytes of storage.

package mainimport ("fmt""unsafe")funcmain() {varsstruct{}fmt.Println(unsafe.Sizeof(s))// Output: 0}

Methods

Let's talk about methods, sometimes also known as function receivers.

Technically, Go is not an object-oriented programming language. It doesn't have classes, objects, and inheritance.

However, Go has types. And, you can definemethods on types.

A method is nothing but a function with a specialreceiver argument. Let's see how we can declare methods.

func (variableT)Name(params) (returnTypes) {}

Thereceiver argument has a name and a type. It appears between thefunc keyword and the method name.

For example, let's define aCar struct.

typeCarstruct {NamestringYearint}

Now, let us define a method likeIsLatest which will tell us if a car was manufactured within the last 5 years.

func (cCar)IsLatest()bool {returnc.Year>=2017}

As you can see, we can access the instance ofCar using the receiver variablec. I like to think of it asthis keyword from the object-oriented world.

Now we should be able to call this method after we initialize our struct, just like we do with classes in other languages.

funcmain() {c:=Car{"Tesla",2021}fmt.Println("IsLatest",c.IsLatest())}

Methods with Pointer receivers

All the examples that we saw previously had a value receiver.

With a value receiver, the method operates on a copy of the value passed to it. Therefore, any modifications done to the receiver inside the methods are not visible to the caller.

For example, let's make another method calledUpdateName which will update the name of theCar.

func (cCar)UpdateName(namestring) {c.Name=name}

Now, let's run this.

funcmain() {c:=Car{"Tesla",2021}c.UpdateName("Toyota")fmt.Println("Car:",c)}
$ go run main.goCar: {Tesla 2021}

Seems like the name wasn't updated, so now let's switch our receiver to pointer type and try again.

func (c*Car)UpdateName(namestring) {c.Name=name}
$ go run main.goCar: {Toyota 2021}

As expected, methods with pointer receivers can modify the value to which the receiver points. Such modifications are visible to the caller of the method as well.

Properties

Let's also see some properties of the methods!

  • Go is smart enough to interpret our function call correctly, and hence, pointer receiver method calls are just syntactic sugar provided by Go for convenience.
(&c).UpdateName(...)
  • We can omit the variable part of the receiver as well if we're not using it.
func (Car)UpdateName(...) {}
  • Methods are not limited to structs but can also be used with non-struct types as well.
package mainimport"fmt"typeMyIntintfunc (iMyInt)isGreater(valueint)bool {returni>MyInt(value)}funcmain() {i:=MyInt(10)fmt.Println(i.isGreater(5))}

Why methods instead of functions?

So the question is, why use methods instead of functions?

As always, there's no particular answer for this, and in no way one is better than the other. Instead, they should be used appropriately when the situation arrives.

One thing I can think of right now is that methods can help us avoid naming conflicts.

Since a method is tied to a particular type, we can have the same method names for multiple receivers.

But in the end, it might just come down to preference, such as"method calls are much easier to read and understand than function calls" or the other way around.

Arrays and Slices

In this tutorial, we will learn about arrays and slices in Go.

Arrays

What is an array?

An array is a fixed-size collection of elements of the same type. The elements of the array are stored sequentially and can be accessed using theirindex.

array

Declaration

We can declare an array as follows:

vara [n]T

Here,n is the length andT can be any type like integer, string, or user-defined structs.

Now, let's declare an array of integers with length 4 and print it.

funcmain() {vararr [4]intfmt.Println(arr)}
$ go run main.go[0 0 0 0]

By default, all the array elements are initialized with the zero value of the corresponding array type.

Initialization

We can also initialize an array using an array literal.

vara [n]T= [n]T{V1,V2,...Vn}
funcmain() {vararr= [4]int{1,2,3,4}fmt.Println(arr)}
$ go run main.go[1 2 3 4]

We can even do a shorthand declaration.

...arr:= [4]int{1,2,3,4}

Access

And similar to other languages, we can access the elements using theindex as they're stored sequentially.

funcmain() {arr:= [4]int{1,2,3,4}fmt.Println(arr[0])}
$ go run main.go1

Iteration

Now, let's talk about iteration.

So, there are multiple ways to iterate over arrays.

The first one is using the for loop with thelen function which gives us the length of the array.

funcmain() {arr:= [4]int{1,2,3,4}fori:=0;i<len(arr);i++ {fmt.Printf("Index: %d, Element: %d\n",i,arr[i])}}
$ go run main.goIndex: 0, Element: 1Index: 1, Element: 2Index: 2, Element: 3Index: 3, Element: 4

Another way is to use therange keyword with thefor loop.

funcmain() {arr:= [4]int{1,2,3,4}fori,e:=rangearr {fmt.Printf("Index: %d, Element: %d\n",i,e)}}
$ go run main.goIndex: 0, Element: 1Index: 1, Element: 2Index: 2, Element: 3Index: 3, Element: 4

As we can see, our example works the same as before.

But the range keyword is quite versatile and can be used in multiple ways.

fori,e:=rangearr {}// Normal usage of rangefor_,e:=rangearr {}// Omit index with _ and use elementfori:=rangearr {}// Use index onlyforrangearr {}// Simply loop over the array

Multi dimensional

All the arrays that we created so far are one-dimensional. We can also create multi-dimensional arrays in Go.

Let's take a look at an example:

funcmain() {arr:= [2][4]int{{1,2,3,4},{5,6,7,8},}fori,e:=rangearr {fmt.Printf("Index: %d, Element: %d\n",i,e)}}
$ go run main.goIndex: 0, Element: [1 2 3 4]Index: 1, Element: [5 6 7 8]

We can also let the compiler infer the length of the array by using... ellipses instead of the length.

funcmain() {arr:= [...][4]int{{1,2,3,4},{5,6,7,8},}fori,e:=rangearr {fmt.Printf("Index: %d, Element: %d\n",i,e)}}
$ go run main.goIndex: 0, Element: [1 2 3 4]Index: 1, Element: [5 6 7 8]

Properties

Now let's talk about some properties of arrays.

The array's length is part of its type. So, the arraya andb are completely distinct types, and we cannot assign one to the other.

This also means that we cannot resize an array, because resizing an array would mean changing its type.

package mainfuncmain() {vara= [4]int{1,2,3,4}varb [2]int=a// Error, cannot use a (type [4]int) as type [2]int in assignment}

Arrays in Go are value types unlike other languages like C, C++, and Java where arrays are reference types.

This means that when we assign an array to a new variable or pass an array to a function, the entire array is copied.

So, if we make any changes to this copied array, the original array won't be affected and will remain unchanged.

package mainimport"fmt"funcmain() {vara= [7]string{"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}varb=a// Copy of a is assigned to bb[0]="Monday"fmt.Println(a)// Output: [Mon Tue Wed Thu Fri Sat Sun]fmt.Println(b)// Output: [Monday Tue Wed Thu Fri Sat Sun]}

Slices

I know what you're thinking, arrays are useful but a bit inflexible due to the limitation caused by their fixed size.

This brings us to Slice, so what is a slice?

A Slice is a segment of an array. Slices build on arrays and provide more power, flexibility, and convenience.

slice

A slice consists of three things:

  • A pointer reference to an underlying array.
  • The length of the segment of the array that the slice contains.
  • And, the capacity, which is the maximum size up to which the segment can grow.

Just likelen function, we can determine the capacity of a slice using the built-incap function. Here's an example:

package mainimport"fmt"funcmain() {a:= [5]int{20,15,5,30,25}s:=a[1:4]// Output: Array: [20 15 5 30 25], Length: 5, Capacity: 5fmt.Printf("Array: %v, Length: %d, Capacity: %d\n",a,len(a),cap(a))// Output: Slice [15 5 30], Length: 3, Capacity: 4fmt.Printf("Slice: %v, Length: %d, Capacity: %d",s,len(s),cap(s))}

Don't worry, we are going to discuss everything shown here in detail.

Declaration

Let's see how we can declare a slice.

vars []T

As we can see, we don't need to specify any length. Let's declare a slice of integers and see how it works.

funcmain() {vars []stringfmt.Println(s)fmt.Println(s==nil)}
$ go run main.go[]true

So, unlike arrays, the zero value of a slice isnil.

Initialization

There are multiple ways to initialize our slice. One way is to use the built-inmake function.

make([]T,len,cap) []T
funcmain() {vars=make([]string,0,0)fmt.Println(s)}
$ go run main.go[]

Similar to arrays, we can use the slice literal to initialize our slice.

funcmain() {vars= []string{"Go","TypeScript"}fmt.Println(s)}
$ go run main.go[Go TypeScript]

Another way is to create a slice from an array. Since a slice is a segment of an array, we can create a slice from indexlow tohigh as follows.

a[low:high]
funcmain() {vara= [4]string{"C++","Go","Java","TypeScript",}s1:=a[0:2]// Select from 0 to 2s2:=a[:3]// Select first 3s3:=a[2:]// Select last 2fmt.Println("Array:",a)fmt.Println("Slice 1:",s1)fmt.Println("Slice 2:",s2)fmt.Println("Slice 3:",s3)}
$ go run main.goArray: [C++ Go Java TypeScript]Slice 1: [C++ Go]Slice 2: [C++ Go Java]Slice 3: [Java TypeScript]

Missing low index implies 0 and missing high index implies the length of the underlying array (len(a)).

The thing to note here is we can create a slice from other slices too and not just arrays.

vara= []string{"C++","Go","Java","TypeScript",}

Iteration

We can iterate over a slice in the same way you iterate over an array, by using the for loop with eitherlen function orrange keyword.

Functions

So now, let's talk about built-in slice functions provided in Go.

copy

Thecopy() function copies elements from one slice to another. It takes 2 slices, a destination, and a source. It also returns the number of elements copied.

funccopy(dst,src []T)int

Let's see how we can use it.

funcmain() {s1:= []string{"a","b","c","d"}s2:=make([]string,len(s1))e:=copy(s2,s1)fmt.Println("Src:",s1)fmt.Println("Dst:",s2)fmt.Println("Elements:",e)}
$ go run main.goSrc: [a b c d]Dst: [a b c d]Elements: 4

As expected, our 4 elements from the source slice were copied to the destination slice.

append

Now, let's look at how we can append data to our slice using the built-inappend function which appends new elements at the end of a given slice.

It takes a slice and a variable number of arguments. It then returns a new slice containing all the elements.

append(slice []T,elems...T) []T

Let's try it in an example by appending elements to our slice.

funcmain() {s1:= []string{"a","b","c","d"}s2:=append(s1,"e","f")fmt.Println("s1:",s1)fmt.Println("s2:",s2)}
$ go run main.gos1: [a b c d]s2: [a b c d e f]

As we can see, the new elements were appended and a new slice was returned.

But if the given slice doesn't have sufficient capacity for the new elements then a new underlying array is allocated with a bigger capacity.

All the elements from the underlying array of the existing slice are copied to this new array, and then the new elements are appended.

Properties

Finally, let's discuss some properties of slices.

Slices are reference types, unlike arrays.

This means modifying the elements of a slice will modify the corresponding elements in the referenced array.

package mainimport"fmt"funcmain() {a:= [7]string{"Mon","Tue","Wed","Thu","Fri","Sat","Sun"}s:=a[0:2]s[0]="Sun"fmt.Println(a)// Output: [Sun Tue Wed Thu Fri Sat Sun]fmt.Println(s)// Output: [Sun Tue]}

Slices can be used with variadic types as well.

package mainimport"fmt"funcmain() {values:= []int{1,2,3}sum:=add(values...)fmt.Println(sum)}funcadd(values...int)int {sum:=0for_,v:=rangevalues {sum+=v}returnsum}

Maps

So, Go provides a built-in map type, and we'll learn how to use it.

But, the question is what are maps? And why do we need them?

maps

Well, A map is an unordered collection of key-value pairs. It maps keys to values. The keys are unique within a map while the values may not be.

It is used for fast lookups, retrieval, and deletion of data based on keys. It is one of the most used data structures.

Declaration

Let's start with the declaration.

A map is declared using the following syntax:

varmmap[K]V

WhereK is the key type andV is the value type.

For example, here's how we can declare a map ofstring keys toint values.

funcmain() {varmmap[string]intfmt.Println(m)}
$ go run main.gonil

As we can see, the zero value of a map isnil.

Anil map has no keys. Moreover, any attempt to add keys to anil map will result in a runtime error.

Initialization

There are multiple ways to initialize a map.

make function

We can use the built-inmake function, which allocates memory for referenced data types and initializes their underlying data structures.

funcmain() {varm=make(map[string]int)fmt.Println(m)}
$ go run main.gomap[]

map literal

Another way is using a map literal.

funcmain() {varm=map[string]int{"a":0,"b":1,}fmt.Println(m)}

Note that the trailing comma is required.

$ go run main.gomap[a:0 b:1]

As always, we can use our custom types as well.

typeUserstruct {Namestring}funcmain() {varm=map[string]User{"a":User{"Peter"},"b":User{"Seth"},}fmt.Println(m)}

We can even remove the value type and Go will figure it out!

varm=map[string]User{"a": {"Peter"},"b": {"Seth"},}
$ go run main.gomap[a:{Peter} b:{Seth}]

Add

Now, let's see how we can add a value to our map.

funcmain() {varm=map[string]User{"a": {"Peter"},"b": {"Seth"},}m["c"]=User{"Steve"}fmt.Println(m)}
$ go run main.gomap[a:{Peter} b:{Seth} c:{Steve}]

Retrieve

We can also retrieve our values from the map using the key.

...c:=m["c"]fmt.Println("Key c:",c)
$ go run main.gokey c: {Steve}

What if we use a key that is not present in the map?

...d:=m["d"]fmt.Println("Key d:",d)

Yes, you guessed it! we will get the zero value of the map's value type.

$ go run main.goKey c: {Steve}Key d: {}

Exists

When you retrieve the value assigned to a given key, it returns an additional boolean value as well. The boolean variable will betrue if the key exists, andfalse otherwise.

Let's try this in an example:

...c,ok:=m["c"]fmt.Println("Key c:",c,ok)d,ok:=m["d"]fmt.Println("Key d:",d,ok)
$ go run main.goKey c: {Steve} Present:trueKey d: {} Present:false

Update

We can also update the value for a key by simply re-assigning a key.

...m["a"]="Roger"
$ go run main.gomap[a:{Roger} b:{Seth} c:{Steve}]

Delete

Or, we can delete the key using the built-indelete function.

Here's how the syntax looks:

...delete(m,"a")

The first argument is the map, and the second is the key we want to delete.

Thedelete() function doesn't return any value. Also, it doesn't do anything if the key doesn't exist in the map.

$ go run main.gomap[a:{Roger} c:{Steve}]

Iteration

Similar to arrays or slices, we can iterate over maps with therange keyword.

package mainimport"fmt"funcmain() {varm=map[string]User{"a": {"Peter"},"b": {"Seth"},}m["c"]=User{"Steve"}forkey,value:=rangem {fmt.Println("Key: %s, Value: %v",key,value)}}
$ go run main.goKey: c, Value: {Steve}Key: a, Value: {Peter}Key: b, Value: {Seth}

Note that a map is an unordered collection, and therefore the iteration order of a map is not guaranteed to be the same every time we iterate over it.

Properties

Lastly, let's talk about map properties.

Maps are reference types, which means when we assign a map to a new variable, they both refer to the same underlying data structure.

Therefore, changes done by one variable will be visible to the other.

package mainimport"fmt"typeUserstruct {Namestring}funcmain() {varm1=map[string]User{"a": {"Peter"},"b": {"Seth"},}m2:=m1m2["c"]=User{"Steve"}fmt.Println(m1)// Output: map[a:{Peter} b:{Seth} c:{Steve}]fmt.Println(m2)// Output: map[a:{Peter} b:{Seth} c:{Steve}]}

Interfaces

In this section, let's talk about the interfaces.

What is an interface?

So, an interface in Go is anabstract type that is defined using a set of method signatures. The interface defines thebehavior for similar types of objects.

Here,behavior is a key term that we will discuss shortly.

Let's take a look at an example to understand this better.

One of the best real-world examples of interfaces is the power socket. Imagine that we need to connect different devices to the power socket.

no-interface

Let's try to implement this. Here are the device types we will be using.

typemobilestruct {brandstring}typelaptopstruct {cpustring}typetoasterstruct {amountint}typekettlestruct {quantitystring}typesocketstruct{}

Now, let's define aDraw method on a type, let's saymobile. Here we will simply print the properties of the type.

func (mmobile)Draw(powerint) {fmt.Printf("%T -> brand: %s, power: %d",m,m.brand,power)}

Great, now we will define thePlug method on thesocket type which accepts ourmobile type as an argument.

func (socket)Plug(devicemobile,powerint) {device.Draw(power)}

Let's try to"connect" or"plug in" themobile type to oursocket type in themain function.

package mainimport"fmt"funcmain() {m:=mobile{"Apple"}s:=socket{}s.Plug(m,10)}

And if we run this we'll see the following.

$ go run main.gomain.mobile -> brand: Apple, power: 10

This is interesting, but let's say now we want to connect ourlaptop type.

package mainimport"fmt"funcmain() {m:=mobile{"Apple"}l:=laptop{"Intel i9"}s:=socket{}s.Plug(m,10)s.Plug(l,50)// Error: cannot use l as mobile value in argument}

As we can see, this will throw an error.

What should we do now? Define another method? Such asPlugLaptop?

Sure, but then every time we add a new device type we will need to add a new method to the socket type as well and that's not ideal.

This is where theinterface comes in. Essentially, we want to define acontract that, in the future, must be implemented.

We can simply define an interface such asPowerDrawer and use it in ourPlug function to allow any device that satisfies the criteria, which is that the type must have aDraw method matching the signature that the interface requires.

And anyways, the socket doesn't need to know anything about our device and can simply call theDraw method.

interface

Now let's try to implement ourPowerDrawer interface. Here's how it will look.

The convention is to use"-er" as a suffix in the name. And as we discussed earlier, an interface should only describe theexpected behavior. Which in our case is theDraw method.

interface-implementation

typePowerDrawerinterface {Draw(powerint)}

Now, we need to update ourPlug method to accept a device that implements thePowerDrawer interface as an argument.

func (socket)Plug(devicePowerDrawer,powerint) {device.Draw(power)}

And to satisfy the interface, we can simply addDraw methods to all the device types.

typemobilestruct {brandstring}func (mmobile)Draw(powerint) {fmt.Printf("%T -> brand: %s, power: %d\n",m,m.brand,power)}typelaptopstruct {cpustring}func (llaptop)Draw(powerint) {fmt.Printf("%T -> cpu: %s, power: %d\n",l,l.cpu,power)}typetoasterstruct {amountint}func (ttoaster)Draw(powerint) {fmt.Printf("%T -> amount: %d, power: %d\n",t,t.amount,power)}typekettlestruct {quantitystring}func (kkettle)Draw(powerint) {fmt.Printf("%T -> quantity: %s, power: %d\n",k,k.quantity,power)}

Now, we can connect all our devices to the socket with the help of our interface!

funcmain() {m:=mobile{"Apple"}l:=laptop{"Intel i9"}t:=toaster{4}k:=kettle{"50%"}s:=socket{}s.Plug(m,10)s.Plug(l,50)s.Plug(t,30)s.Plug(k,25)}

And it works just as we expected.

$ go run main.gomain.mobile -> brand: Apple, power: 10main.laptop -> cpu: Intel i9, power: 50main.toaster -> amount: 4, power: 30main.kettle -> quantity: Half Empty, power: 25

But why is this considered such a powerful concept?

Well, an interface can help us decouple our types. For example, because we have the interface, we don't need to update oursocket implementation. We can just define a new device type with aDraw method.

Unlike other languages, Go Interfaces are implementedimplicitly, so we don't need something like animplements keyword. This means that a type satisfies an interface automatically when it has"all the methods" of the interface.

Empty Interface

Next, let's talk about the empty interface. An empty interface can take on a value of any type.

Here's how we declare it.

varxinterface{}

But why do we need it?

Empty interfaces can be used to handle values of unknown types.

Some examples are:

  • Reading heterogeneous data from an API.
  • Variables of an unknown type, like in thefmt.Println function.

To use a value of type emptyinterface{}, we can usetype assertion or atype switch to determine the type of the value.

Type Assertion

Atype assertion provides access to an interface value's underlying concrete value.

For example:

funcmain() {variinterface{}="hello"s:=i.(string)fmt.Println(s)}

This statement asserts that the interface value holds a concrete type and assigns the underlying type value to the variable.

We can also test whether an interface value holds a specific type.

A type assertion can return two values:

  • The first one is the underlying value.
  • The second is a boolean value that reports whether the assertion succeeded.
s,ok:=i.(string)fmt.Println(s,ok)

This can help us test whether an interface value holds a specific type or not.

In a way, this is similar to how we read values from a map.

And If this is not the case then,ok will be false and the value will be the zero value of the type, and no panic will occur.

f,ok:=i.(float64)fmt.Println(f,ok)

But if the interface does not hold the type, the statement will trigger a panic.

f=i.(float64)fmt.Println(f)// Panic!
$ go run main.gohellohellotrue0falsepanic: interface conversion: interface {} is string, not float64

Type Switch

Here, aswitch statement can be used to determine the type of a variable of type emptyinterface{}.

vartinterface{}t="hello"switcht:=t.(type) {casestring:fmt.Printf("string: %s\n",t)casebool:fmt.Printf("boolean: %v\n",t)caseint:fmt.Printf("integer: %d\n",t)default:fmt.Printf("unexpected: %T\n",t)}

And if we run this, we can verify that we have astring type.

$ go run main.gostring: hello

Properties

Let's discuss some properties of interfaces.

Zero value

The zero value of an interface isnil.

package mainimport"fmt"typeMyInterfaceinterface {Method()}funcmain() {variMyInterfacefmt.Println(i)// Output: <nil>}

Embedding

We can embed interfaces like structs. For example:

typeinterface1interface {Method1()}typeinterface2interface {Method2()}typeinterface3interface {interface1interface2}

Values

Interface values are comparable.

package mainimport"fmt"typeMyInterfaceinterface {Method()}typeMyTypestruct{}func (MyType)Method() {}funcmain() {t:=MyType{}variMyInterface=MyType{}fmt.Println(t==i)}

Interface Values

Under the hood, an interface value can be thought of as a tuple consisting of a value and a concrete type.

package mainimport"fmt"typeMyInterfaceinterface {Method()}typeMyTypestruct {propertyint}func (MyType)Method() {}funcmain() {variMyInterfacei=MyType{10}fmt.Printf("(%v, %T)\n",i,i)// Output: ({10}, main.MyType)}

With that, we covered interfaces in Go.

It's a really powerful feature, but remember,"Bigger the interface, the weaker the abstraction" - Rob Pike.

Errors

In this tutorial, let's talk about error handling.

Notice how I said errors and not exceptions as there is no exception handling in Go.

Instead, we can just return a built-inerror type which is an interface type.

typeerrorinterface {Error()string}

We will circle back to this shortly. First, let's try to understand the basics.

So, let's declare a simpleDivide function which, as the name suggests, will divide integera byb.

funcDivide(a,bint)int {returna/b}

Great. Now, we want to return an error, let's say, to prevent the division by zero. This brings us to error construction.

Constructing Errors

There are multiple ways to do this, but we will look at the two most common ones.

errors package

The first is by using theNew function provided by theerrors package.

package mainimport"errors"funcmain() {}funcDivide(a,bint) (int,error) {ifb==0 {return0,errors.New("cannot divide by zero")}returna/b,nil}

Notice, how we return anerror with the result. And if there is no error we simply returnnil as it is the zero value of an error because after all, it's an interface.

But how do we handle it? So, for that, let's call theDivide function in ourmain function.

package mainimport ("errors""fmt")funcmain() {result,err:=Divide(4,0)iferr!=nil {fmt.Println(err)// Do something with the errorreturn}fmt.Println(result)// Use the result}funcDivide(a,bint) (int,error) {...}
$ go run main.gocannot divide by zero

As you can see, we simply check if the error isnil and build our logic accordingly. This is considered quite idiomatic in Go and you will see this being used a lot.

Another way to construct our errors is by using thefmt.Errorf function.

This function is similar tofmt.Sprintf and it lets us format our error. But instead of returning a string, it returns an error.

It is often used to add some context or detail to our errors.

...funcDivide(a,bint) (int,error) {ifb==0 {return0,fmt.Errorf("cannot divide %d by zero",a)}returna/b,nil}

And it should work similarly.

$ go run main.gocannot divide 4 by zero

Sentinel Errors

Another important technique in Go is defining expected Errors so they can be checked explicitly in other parts of the code. These are sometimes referred to as sentinel errors.

package mainimport ("errors""fmt")varErrDivideByZero=errors.New("cannot divide by zero")funcmain() {...}funcDivide(a,bint) (int,error) {ifb==0 {return0,ErrDivideByZero}returna/b,nil}

In Go, it is considered conventional to prefix the variable withErr. For example,ErrNotFound.

But what's the point?

So, this becomes useful when we need to execute a different branch of code if a certain kind of error is encountered.

For example, now we can check explicitly which error occurred using theerrors.Is function.

package mainimport ("errors""fmt")funcmain() {result,err:=Divide(4,0)iferr!=nil {switch {caseerrors.Is(err,ErrDivideByZero):fmt.Println(err)// Do something with the errordefault:fmt.Println("no idea!")    }return}fmt.Println(result)// Use the result}funcDivide(a,bint) (int,error) {...}
$ go run main.gocannot divide by zero

Custom Errors

This strategy covers most of the error handling use cases. But sometimes we need additional functionalities such as dynamic values inside of our errors.

Earlier, we saw thaterror is just an interface. So basically, anything can be anerror as long as it implements theError() method which returns an error message as a string.

So, let's define our customDivisionError struct which will contain an error code and a message.

package mainimport ("errors""fmt")typeDivisionErrorstruct {CodeintMsgstring}func (dDivisionError)Error()string {returnfmt.Sprintf("code %d: %s",d.Code,d.Msg)}funcmain() {...}funcDivide(a,bint) (int,error) {ifb==0 {return0,DivisionError{Code:2000,Msg:"cannot divide by zero",}}returna/b,nil}

Here, we will useerrors.As instead oferrors.Is function to convert the error to the correct type.

funcmain() {result,err:=Divide(4,0)iferr!=nil {vardivErrDivisionErrorswitch {caseerrors.As(err,&divErr):fmt.Println(divErr)// Do something with the errordefault:fmt.Println("no idea!")}return}fmt.Println(result)// Use the result}funcDivide(a,bint) (int,error) {...}
$ go run main.gocode 2000: cannot divide by zero

But what's the difference betweenerrors.Is anderrors.As?

The difference is that this function checks whether the error has a specific type, unlike theIs function, which examines if it is a particular error object.

We can also use type assertions but it's not preferred.

funcmain() {result,err:=Divide(4,0)ife,ok:=err.(DivisionError);ok {fmt.Println(e.Code,e.Msg)// Output: 2000 cannot divide by zeroreturn}fmt.Println(result)}

Lastly, I will say that error handling in Go is quite different compared to the traditionaltry/catch idiom in other languages. But it is very powerful as it encourages the developer to actually handle the error in an explicit way, which improves readability as well.

Panic and Recover

So earlier, we learned that the idiomatic way of handling abnormal conditions in a Go program is using errors. While errors are sufficient for most cases, there are some situations where the program cannot continue.

In those cases, we can use the built-inpanic function.

Panic

funcpanic(interface{})

The panic is a built-in function that stops the normal execution of the currentgoroutine. When a function callspanic, the normal execution of the function stops immediately and the control is returned to the caller. This is repeated until the program exits with the panic message and stack trace.

Note: We will discussgoroutines later in the course.

So, let's see how we can use thepanic function.

package mainfuncmain() {WillPanic()}funcWillPanic() {panic("Woah")}

And if we run this, we can seepanic in action.

$ go run main.gopanic: Woahgoroutine 1 [running]:main.WillPanic(...)        .../main.go:8main.main()        .../main.go:4 +0x38exit status 2

As expected, our program printed the panic message, followed by the stack trace, and then it was terminated.

So, the question is, what to do when an unexpected panic happens?

Recover

Well, it is possible to regain control of a panicking program using the built-inrecover function, along with thedefer keyword.

funcrecover()interface{}

Let's try an example by creating ahandlePanic function. And then, we can call it usingdefer.

package mainimport"fmt"funcmain() {WillPanic()}funchandlePanic() {data:=recover()fmt.Println("Recovered:",data)}funcWillPanic() {deferhandlePanic()panic("Woah")}
$ go run main.goRecovered: Woah

As we can see, our panic was recovered and now our program can continue execution.

Lastly, I will mention thatpanic andrecover can be considered similar to thetry/catch idiom in other languages. But one important factor is that we should avoid panic and recover and useerrors when possible.

If so, then this brings us to the question, when should we usepanic?

Use Cases

There are two valid use cases forpanic:

  • An unrecoverable error

Which can be a situation where the program cannot simply continue its execution.

For example, reading a configuration file which is important to start the program, as there is nothing else to do if the file read itself fails.

  • Developer error

This is the most common situation. For example, dereferencing a pointer when the value isnil will cause a panic.

Testing

In this tutorial, we will talk about testing in Go. So, let's start using a simple example.

We have created amath package that contains anAdd function Which as the name suggests, adds two integers.

package mathfuncAdd(a,bint)int {returna+b}

It's being used in ourmain package like this.

package mainimport ("example/math""fmt")funcmain() {result:=math.Add(2,2)fmt.Println(result)}

And, if we run this, we should see the result.

$ go run main.go4

Now, we want to test ourAdd function. So, in Go, we declare test files with_test suffix in the file name. So for ouradd.go, we will create a test asadd_test.go. Our project structure should look like this.

.├── go.mod├── main.go└── math    ├── add.go    └── add_test.go

We will start by using amath_test package, and importing thetesting package from the standard library. That's right! Testing is built into Go, unlike many other languages.

But wait...why do we need to usemath_test as our package, can't we just use the samemath package?

Well yes, we can write our test in the same package if we wanted, but I personally think doing this in a separate package helps us write tests in a more decoupled way.

Now, we can create ourTestAdd function. It will take an argument of typetesting.T which will provide us with helpful methods.

package math_testimport"testing"funcTestAdd(t*testing.T) {}

Before we add any testing logic, let's try to run it. But this time, we cannot usego run command, instead, we will use thego test command.

$ gotest ./mathok      example/math 0.429s

Here, we will have our package name which ismath, but we can also use the relative path./... to test all packages.

$ gotest ./...?       example [notest files]ok      example/math 0.348s

And if Go doesn't find any test in a package, it will let us know.

Perfect, let's write some test code. To do this, we will check our result with an expected value and if they do not match, we can use thet.Fail method to fail the test.

package math_testimport"testing"funcTestAdd(t*testing.T) {got:=math.Add(1,1)expected:=2ifgot!=expected {t.Fail()}}

Great! Our test seems to have passed.

$ gotest mathok      example/math    0.412s

Let's also see what happens if we fail the test, for that, we can simply change our expected result.

package math_testimport"testing"funcTestAdd(t*testing.T) {got:=math.Add(1,1)expected:=3ifgot!=expected {t.Fail()}}
$ gotest ./mathok      example/math    (cached)

If you see this, don't worry. For optimization, our tests are cached. We can use thego clean command to clear our cache and then re-run the test.

$ go clean -testcache$ gotest ./math--- FAIL: TestAdd (0.00s)FAILFAIL    example/math    0.354sFAIL

So, this is what a test failure will look like.

Table driven tests

This brings us to table-driven tests. But what exactly are they?

So earlier, we had function arguments and expected variables which we compared to determine if our tests passed or fail. But what if we defined all that in a slice and iterate over that? This will make our tests a little bit more flexible and help us run multiple cases easily.

Don't worry, we will learn this by example. So we will start by defining ouraddTestCase struct.

package math_testimport ("example/math""testing")typeaddTestCasestruct {a,b,expectedint}vartestCases= []addTestCase{{1,1,3},{25,25,50},{2,1,3},{1,10,11},}funcTestAdd(t*testing.T) {for_,tc:=rangetestCases {got:=math.Add(tc.a,tc.b)ifgot!=tc.expected {t.Errorf("Expected %d but got %d",tc.expected,got)}}}

Notice, how we declaredaddTestCase with a lower case. That's right we don't want to export it as it's not useful outside our testing logic. Let's run our test.

$ go run main.go--- FAIL: TestAdd (0.00s)    add_test.go:25: Expected 3 but got 2FAILFAIL    example/math    0.334sFAIL

Seems like our tests broke, let's fix them by updating our test cases.

vartestCases= []addTestCase{{1,1,2},{25,25,50},{2,1,3},{1,10,11},}

Perfect, it's working!

$ go run main.gook      example/math    0.589s

Code coverage

Finally, let's talk about code coverage. When writing tests, it is often important to know how much of your actual code the tests cover. This is generally referred to as code coverage.

To calculate and export the coverage for our test, we can simply use the-coverprofile argument with thego test command.

$ gotest ./math -coverprofile=coverage.outok      example/math    0.385s  coverage: 100.0% of statements

Seems like we have great coverage. Let's also check the report using thego tool cover command which gives us a detailed report.

$ go tool cover -html=coverage.out

coverage

As we can see, this is a much more readable format. And best of all, it is built right into standard tooling.

Fuzz testing

Lastly, let's look at fuzz testing which was introduced in Go version 1.18.

Fuzzing is a type of automated testing that continuously manipulates inputs to a program to find bugs.

Go fuzzing uses coverage guidance to intelligently walk through the code being fuzzed to find and report failures to the user.

Since it can reach edge cases that humans often miss, fuzz testing can be particularly valuable for finding bugs and security exploits.

Let's try an example:

funcFuzzTestAdd(f*testing.F) {f.Fuzz(func(t*testing.T,a,bint) {math.Add(a ,b)})}

If we run this, we'll see that it'll automatically create test cases. Because ourAdd function is quite simple, tests will pass.

$ gotest -fuzz FuzzTestAdd example/mathfuzz: elapsed: 0s, gathering baseline coverage: 0/192 completedfuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workersfuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)fuzz: elapsed: 9s, execs: 1039901 (119895/sec), new interesting: 19 (total: 210)fuzz: elapsed: 12s, execs: 1386684 (115594/sec), new interesting: 21 (total: 212)PASSok      foo 12.692s

But if we update ourAdd function with a random edge case such that the program will panic ifb + 10 is greater thana.

funcAdd(a,bint)int {ifa>b+10 {panic("B must be greater than A")}returna+b}

And if we re-run the test, this edge case will be caught by fuzz testing.

$ gotest -fuzz FuzzTestAdd example/mathwarning: starting with empty corpusfuzz: elapsed: 0s, execs: 0 (0/sec), new interesting: 0 (total: 0)fuzz: elapsed: 0s, execs: 1 (25/sec), new interesting: 0 (total: 0)--- FAIL: FuzzTestAdd (0.04s)    --- FAIL: FuzzTestAdd (0.00s)        testing.go:1349: panic: B is greater than A

I think this is a really cool feature of Go 1.18. You can learn more about fuzz testing from theofficial Go blog.

Generics

In this section, we will learn about Generics which is a much awaited feature that was released with Go version 1.18.

What are Generics?

Generics means parameterized types. Put simply, generics allow programmers to write code where the type can be specified later because the type isn't immediately relevant.

Let's take a look at an example to understand this better.

For our example, we have simple sum functions for different types such asint,float64, andstring. Since method overriding is not allowed in Go we usually have to create new functions.

package mainimport"fmt"funcsumInt(a,bint)int {returna+b}funcsumFloat(a,bfloat64)float64 {returna+b}funcsumString(a,bstring)string {returna+b}funcmain() {fmt.Println(sumInt(1,2))fmt.Println(sumFloat(4.0,2.0))fmt.Println(sumString("a","b"))}

As we can see, apart from the types, these functions are pretty similar.

Let's see how we can define a generic function.

funcfnName[Tconstraint]() {...}

Here,T is our type parameter andconstraint will be the interface that allows any type implementing the interface.

I know, I know, this is confusing. So, let's start building our genericsum function.

Here, we will useT as our type parameter with an emptyinterface{} as our constraint.

funcsum[Tinterface{}](a,bT)T {fmt.Println(a,b)}

Also, starting with Go 1.18 we can useany, which is pretty much equivalent to the empty interface.

funcsum[Tany](a,bT)T {fmt.Println(a,b)}

With type parameters, comes the need to pass type arguments, which can make our code verbose.

sum[int](1,2)// explicit type argumentsum[float64](4.0,2.0)sum[string]("a","b")

Luckily, Go 1.18 comes withtype inference which helps us to write code that calls generic functions without explicit types.

sum(1,2)sum(4.0,2.0)sum("a","b")

Let's run this and see if it works.

$ go run main.go1 24 2a b

Now, let's update thesum function to add our variables.

funcsum[Tany](a,bT)T {returna+b}
fmt.Println(sum(1,2))fmt.Println(sum(4.0,2.0))fmt.Println(sum("a","b"))

But now if we run this, we will get an error that operator+ is not defined in the constraint.

$ go run main.go./main.go:6:9: invalid operation: operator + not defined on a (variable oftype T constrained by any)

While constraint of typeany generally works it does not support operators.

So let's define our own custom constraint using an interface. Our interface should define a type set containingint,float, andstring.

typeset

Here's how ourSumConstraint interface looks.

typeSumConstraintinterface {int|float64|string}funcsum[TSumConstraint](a,bT)T {returna+b}funcmain() {fmt.Println(sum(1,2))fmt.Println(sum(4.0,2.0))fmt.Println(sum("a","b"))}

And this should work as expected.

$ go run main.go36ab

We can also use theconstraints package which defines a set of useful constraints to be used with type parameters.

typeSignedinterface {~int|~int8|~int16|~int32|~int64}typeUnsignedinterface {~uint|~uint8|~uint16|~uint32|~uint64|~uintptr}typeIntegerinterface {Signed|Unsigned}typeFloatinterface {~float32|~float64}typeComplexinterface {~complex64|~complex128}typeOrderedinterface {Integer|Float|~string}

For that, we will need to install theconstraints package.

$ go get golang.org/x/exp/constraintsgo: added golang.org/x/exp v0.0.0-20220414153411-bcd21879b8fd
import ("fmt""golang.org/x/exp/constraints")funcsum[T constraints.Ordered](a,bT)T {returna+b}funcmain() {fmt.Println(sum(1,2))fmt.Println(sum(4.0,2.0))fmt.Println(sum("a","b"))}

Here we are using theOrdered constraint.

typeOrderedinterface {Integer|Float|~string}

~ is a new token added to Go and the expression~string means the set of all types whose underlying type isstring.

And it still works as expected.

$ go run main.go36ab

Generics is an amazing feature because it permits writing abstract functions that can drastically reduce code duplication in certain cases.

When to use generics

So, when to use generics? We can take the following use cases as an example:

  • Functions that operate on arrays, slices, maps, and channels.
  • General purpose data structures like stack or linked list.
  • To reduce code duplication.

Lastly, I will add that while generics are a great addition to the language, they should be used sparingly.

And, it is advised to start simple and only write generic code once we have written very similar code at least 2 or 3 times.

Concurrency

In this lesson, we will learn about concurrency which is one of the most powerful features of Go.

So, let's start by asking What is"concurrency"?

What is Concurrency

Concurrency, by definition, is the ability to break down a computer program or algorithm into individual parts, which can be executed independently.

The final outcome of a concurrent program is the same as that of a program that has been executed sequentially.

Using concurrency, we can achieve the same results in lesser time, thus increasing the overall performance and efficiency of our programs.

Concurrency vs Parallelism

concurrency-vs-parallelism

A lot of people confuse concurrency with parallelism because they both somewhat imply executing code simultaneously, but they are two completely different concepts.

Concurrency is the task of running and managing multiple computations at the same time, while parallelism is the task of running multiple computations simultaneously.

A simple quote from Rob Pike pretty much sums it up.

"Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once"

But concurrency in Go is more than just syntax. In order to harness the power of Go, we need to first understand how Go approaches concurrent execution of code. Go relies on a concurrency model called CSP (Communicating Sequential Processes).

Communicating Sequential Processes (CSP)

Communicating Sequential Processes (CSP) is a model put forth byTony Hoare in 1978 which describes interactions between concurrent processes. It made a breakthrough in Computer Science, especially in the field of concurrency.

Languages like Go and Erlang have been highly inspired by the concept of communicating sequential processes (CSP).

Concurrency is hard, but CSP allows us to give a better structure to our concurrent code and provides a model for thinking about concurrency in a way that makes it a little easier. Here, processes are independent and they communicate by sharing channels between them.

csp

We'll learn how Golang implements it using goroutines and channels later in the course.

Basic Concepts

Now, let's get familiar with some basic concurrency concepts.

Data Race

A data race occurs when processes have to access the same resource concurrently.

For example, one process reads while another simultaneously writes to the exact same resource.

Race Conditions

A race condition occurs when the timing or order of events affects the correctness of a piece of code.

Deadlocks

A deadlock occurs when all processes are blocked while waiting for each other and the program cannot proceed further.

Coffman Conditions

There are four conditions, known as the Coffman conditions, all of them must be satisfied for a deadlock to occur.

  • Mutual Exclusion

A concurrent process holds at least one resource at any one time making it non-sharable.

In the diagram below, there is a single instance of Resource 1 and it is held by Process 1 only.

mutual-exclusion

  • Hold and wait

A concurrent process holds a resource and is waiting for an additional resource.

In the diagram given below, Process 2 holds Resource 2 and Resource 3 and is requesting the Resource 1 which is held by Process 1.

hold-and-wait

  • No preemption

A resource held by a concurrent process cannot be taken away by the system. It can only be freed by the process holding it.

In the diagram below, Process 2 cannot preempt Resource 1 from Process 1. It will only be released when Process 1 relinquishes it voluntarily after its execution is complete.

no-preemption

  • Circular wait

A process is waiting for the resource held by the second process, which is waiting for the resource held by the third process, and so on, till the last process is waiting for a resource held by the first process. Hence, forming a circular chain.

In the diagram below, Process 1 is allocated Resource2 and it is requesting Resource 1. Similarly, Process 2 is allocated Resource 1 and it is requesting Resource 2. This forms a circular wait loop.

circular-wait

Livelocks

Livelocks are processes that are actively performing concurrent operations, but these operations do nothing to move the state of the program forward.

Starvation

Starvation happens when a process is deprived of necessary resources and is unable to complete its function.

Starvation can happen because of deadlocks or inefficient scheduling algorithms for processes. In order to solve starvation, we need to employ better resource-allotment algorithms that make sure that every process gets its fair share of resources.

Goroutines

In this lesson, we will learn about Goroutines.

But before we start our discussion, I wanted to share an important Go proverb.

"Don't communicate by sharing memory, share memory by communicating." - Rob Pike

What is a goroutine?

Agoroutine is a lightweight thread of execution that is managed by the Go runtime and essentially let us write asynchronous code in a synchronous manner.

It is important to know that they are not actual OS threads and the main function itself runs as a goroutine.

A single thread may run thousands of goroutines in them by using the Go runtime scheduler which uses cooperative scheduling. This implies that if the current goroutine is blocked or has been completed, the scheduler will move the other goroutines to another OS thread. Hence, we achieve efficiency in scheduling where no routine is blocked forever.

We can turn any function into a goroutine by simply using thego keyword.

gofn(x,y,z)

Before we write any code, it is important to briefly discuss the fork-join model.

Fork-Join Model

Go uses the idea of the fork-join model of concurrency behind goroutines. The fork-join model essentially implies that a child process splits from its parent process to run concurrently with the parent process. After completing its execution, the child process merges back into the parent process. The point where it joins back is called thejoin point.

fork-join

Now, let's write some code and create our own goroutine.

package mainimport"fmt"funcspeak(argstring) {fmt.Println(arg)}funcmain() {gospeak("Hello World")}

Here thespeak function call is prefixed with thego keyword. This will allow it to run as a separate goroutine. And that's it, we just created our first goroutine. It's that simple!

Great, let's run this:

$ go run main.go

Interesting, it seems like our program did not run completely as it's missing some output. This is because our main goroutine exited and did not wait for the goroutine that we created.

What if we make our program wait using thetime.Sleep function?

funcmain() {...time.Sleep(1*time.Second)}
$ go run main.goHello World

There we go, we can see our complete output now.

Okay, so this works but it's not ideal. So how do we improve this?

Well, the most tricky part about using goroutines is knowing when they will stop. It is important to know that goroutines run in the same address space, so access to shared memory must be synchronized.

Channels

In this lesson, we will learn about Channels.

So what are channels?

Well, simply defined a channel is a communications pipe between goroutines. Things go in one end and come out another in the same order until the channel is closed.

channel

As we learned earlier, channels in Go are based on Communicating Sequential Processes (CSP).

Creating a channel

Now that we understand what channels are, let's see how we can declare them.

varchchanT

Here, we prefix our typeT which is the data type of the value we want to send and receive with the keywordchan which stands for a channel.

Let's try printing the value of our channelch of typestring.

funcmain() {varchchanstringfmt.Println(ch)}
$ go run main.go<nil>

As we can see, the zero value of a channel isnil and if we try to send data over the channel our program will panic.

So, similar to slices we can initialize our channel using the built-inmake function.

funcmain() {ch:=make(chanstring)fmt.Println(ch)}

And if we run this, we can see our channel was initialized.

$ go run main.go0x1400010e060

Sending and Receiving data

Now that we have a basic understanding of channels, let us implement our earlier example using channels to learn how we can use them to communicate between our goroutines.

package mainimport"fmt"funcspeak(argstring,chchanstring) {ch<-arg// Send}funcmain() {ch:=make(chanstring)gospeak("Hello World",ch)data:=<-ch// Receivefmt.Println(data)}

Notice how we can send data using thechannel<-data and receive data using thedata := <-channel syntax.

$ go run main.goHello World

Perfect, our program ran as we expected.

Buffered Channels

We also have buffered channels that accept a limited number of values without a corresponding receiver for those values.

buffered-channel

Thisbuffer length orcapacity can be specified using the second argument to themake function.

funcmain() {ch:=make(chanstring,2)gospeak("Hello World",ch)gospeak("Hi again",ch)data1:=<-chfmt.Println(data1)data2:=<-chfmt.Println(data2)}

Because this channel is buffered, we can send these values into the channel without a corresponding concurrent receive. This meanssends to a buffered channel block only when the buffer is full andreceives block when the buffer is empty.

By default, a channel is unbuffered and has a capacity of 0, hence, we omit the second argument of themake function.

Next, we have directional channels.

Directional channels

When using channels as function parameters, we can specify if a channel is meant to only send or receive values. This increases the type-safety of our program as by default a channel can both send and receive values.

directional-channels

In our example, we can update ourspeak function's second argument such that it can only send a value.

funcspeak(argstring,chchan<-string) {ch<-arg// Send Only}

Here,chan<- can only be used for sending values and will panic if we try to receive values.

Closing channels

Also, just like any other resource, once we're done with our channel, we need to close it. This can be achieved using the built-inclose function.

Here, we can just pass our channel to theclose function.

funcmain() {ch:=make(chanstring,2)gospeak("Hello World",ch)gospeak("Hi again",ch)data1:=<-chfmt.Println(data1)data2:=<-chfmt.Println(data2)close(ch)}

Optionally, receivers can test whether a channel has been closed by assigning a second parameter to the receive expression.

funcmain() {ch:=make(chanstring,2)gospeak("Hello World",ch)gospeak("Hi again",ch)data1:=<-chfmt.Println(data1)data2,ok:=<-chfmt.Println(data2,ok)close(ch)}

ifok isfalse then there are no more values to receive and the channel is closed.

In a way, this is similar to how we check if a key exists or not in a map.

Properties

Lastly, let's discuss some properties of channels:

  • A send to anil channel blocks forever.
varcchanstringc<-"Hello, World!"// Panic: all goroutines are asleep - deadlock!
  • A receive from anil channel blocks forever.
varcchanstringfmt.Println(<-c)// Panic: all goroutines are asleep - deadlock!
  • A send to a closed channel causes a panic.
varc=make(chanstring,1)c<-"Hello, World!"close(c)c<-"Hello, Panic!"// Panic: send on closed channel
  • A receive from a closed channel returns the zero value immediately.
varc=make(chanint,2)c<-5c<-4close(c)fori:=0;i<4;i++ {fmt.Printf("%d ",<-c)// Output: 5 4 0 0}
  • Range over channels.

We can also usefor andrange to iterate over values received from a channel.

package mainimport"fmt"funcmain() {ch:=make(chanstring,2)ch<-"Hello"ch<-"World"close(ch)fordata:=rangech {fmt.Println(data)}}

Select

In this tutorial, we will learn about theselect statement in Go.

Theselect statement blocks the code and waits for multiple channel operations simultaneously.

Aselect blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

package mainimport ("fmt""time")funcmain() {one:=make(chanstring)two:=make(chanstring)gofunc() {time.Sleep(time.Second*2)one<-"One"}()gofunc() {time.Sleep(time.Second*1)two<-"Two"}()select {caseresult:=<-one:fmt.Println("Received:",result)caseresult:=<-two:fmt.Println("Received:",result)}close(one)close(two)}

Similar toswitch,select also has a default case that runs if no other case is ready. This will help us send or receive without blocking.

funcmain() {one:=make(chanstring)two:=make(chanstring)forx:=0;x<10;x++ {gofunc() {time.Sleep(time.Second*2)one<-"One"}()gofunc() {time.Sleep(time.Second*1)two<-"Two"}()}forx:=0;x<10;x++ {select {caseresult:=<-one:fmt.Println("Received:",result)caseresult:=<-two:fmt.Println("Received:",result)default:fmt.Println("Default...")time.Sleep(200*time.Millisecond)}}close(one)close(two)}

It's also important to know that an emptyselect {} blocks forever.

funcmain() {...select {}close(one)close(two)}

Sync Package

As we learned earlier, goroutines run in the same address space, so access to shared memory must be synchronized. Thesync package provides useful primitives.

WaitGroup

A WaitGroup waits for a collection of goroutines to finish. The main goroutine callsAdd to set the number of goroutines to wait for. Then each of the goroutines runs and callsDone when finished. At the same time,Wait can be used to block until all goroutines have finished.

Usage

We can use thesync.WaitGroup using the following methods:

  • Add(delta int) takes in an integer value which is essentially the number of goroutines that theWaitGroup has to wait for. This must be called before we execute a goroutine.
  • Done() is called within the goroutine to signal that the goroutine has successfully executed.
  • Wait() blocks the program until all the goroutines specified byAdd() have invokedDone() from within.

Example

Let's take a look at an example.

package mainimport ("fmt""sync")funcwork() {fmt.Println("working...")}funcmain() {varwg sync.WaitGroupwg.Add(1)gofunc() {deferwg.Done()work()}()wg.Wait()}

If we run this, we can see our program runs as expected.

$ go run main.goworking...

We can also pass theWaitGroup to the function directly.

funcwork(wg*sync.WaitGroup) {deferwg.Done()fmt.Println("working...")}funcmain() {varwg sync.WaitGroupwg.Add(1)gowork(&wg)wg.Wait()}

But is important to know that aWaitGroupmust not be copied after first use. And if it's explicitly passed into functions, it should be done by apointer. This is because it can affect our counter which will disrupt the logic of our program.

Let's also increase the number of goroutines by calling theAdd method to wait for 4 goroutines.

funcmain() {varwg sync.WaitGroupwg.Add(4)gowork(&wg)gowork(&wg)gowork(&wg)gowork(&wg)wg.Wait()}

And as expected, all our goroutines were executed.

$ go run main.goworking...working...working...working...

Mutex

A Mutex is a mutual exclusion lock that prevents other processes from entering a critical section of data while a process occupies it to prevent race conditions from happening.

What's a critical section?

So, a critical section can be a piece of code that must not be run by multiple threads at once because the code contains shared resources.

Usage

We can usesync.Mutex using the following methods:

  • Lock() acquires or holds the lock.
  • Unlock() releases the lock.
  • TryLock() tries to lock and reports whether it succeeded.

Example

Let's take a look at an example, we will create aCounter struct and add anUpdate method which will update the internal value.

package mainimport ("fmt""sync")typeCounterstruct {valueint}func (c*Counter)Update(nint,wg*sync.WaitGroup) {deferwg.Done()fmt.Printf("Adding %d to %d\n",n,c.value)c.value+=n}funcmain() {varwg sync.WaitGroupc:=Counter{}wg.Add(4)goc.Update(10,&wg)goc.Update(-5,&wg)goc.Update(25,&wg)goc.Update(19,&wg)wg.Wait()fmt.Printf("Result is %d",c.value)}

Let's run this and see what happens.

$ go run main.goAdding -5 to 0Adding 10 to 0Adding 19 to 0Adding 25 to 0Result is 49

That doesn't look accurate, seems like our value is always zero but we somehow got the correct answer.

Well, this is because, in our example, multiple goroutines are updating thevalue variable. And as you must have guessed, this is not ideal.

This is the perfect use case for Mutex. So, let's start by usingsync.Mutex and wrap our critical section in betweenLock() andUnlock() methods.

package mainimport ("fmt""sync")typeCounterstruct {m     sync.Mutexvalueint}func (c*Counter)Update(nint,wg*sync.WaitGroup) {c.m.Lock()deferwg.Done()fmt.Printf("Adding %d to %d\n",n,c.value)c.value+=nc.m.Unlock()}funcmain() {varwg sync.WaitGroupc:=Counter{}wg.Add(4)goc.Update(10,&wg)goc.Update(-5,&wg)goc.Update(25,&wg)goc.Update(19,&wg)wg.Wait()fmt.Printf("Result is %d",c.value)}
$ go run main.goAdding -5 to 0Adding 19 to -5Adding 25 to 14Adding 10 to 39Result is 49

Looks like we solved our issue and the output looks correct as well.

Note: Similar to WaitGroup a Mutexmust not be copied after first use.

RWMutex

An RWMutex is a reader/writer mutual exclusion lock. The lock can be held by an arbitrary number of readers or a single writer.

In other words, readers don't have to wait for each other. They only have to wait for writers holding the lock.

sync.RWMutex is thus preferable for data that is mostly read, and the resource that is saved compared to async.Mutex is time.

Usage

Similar tosync.Mutex, we can usesync.RWMutex using the following methods:

  • Lock() acquires or holds the lock.
  • Unlock() releases the lock.
  • RLock() acquires or holds the read lock.
  • RUnlock() releases the read lock.

Notice how RWMutex has additionalRLock andRUnlock methods compared to Mutex.

Example

Let's add aGetValue method which will read the counter value. We will also changesync.Mutex tosync.RWMutex.

Now, we can simply use theRLock andRUnlock methods so that readers don't have to wait for each other.

package mainimport ("fmt""sync""time")typeCounterstruct {m     sync.RWMutexvalueint}func (c*Counter)Update(nint,wg*sync.WaitGroup) {deferwg.Done()c.m.Lock()fmt.Printf("Adding %d to %d\n",n,c.value)c.value+=nc.m.Unlock()}func (c*Counter)GetValue(wg*sync.WaitGroup) {deferwg.Done()c.m.RLock()deferc.m.RUnlock()fmt.Println("Get value:",c.value)time.Sleep(400*time.Millisecond)}funcmain() {varwg sync.WaitGroupc:=Counter{}wg.Add(4)goc.Update(10,&wg)goc.GetValue(&wg)goc.GetValue(&wg)goc.GetValue(&wg)wg.Wait()}
$ go run main.goGet value: 0Adding 10 to 0Get value: 10Get value: 10

Note: Bothsync.Mutex andsync.RWMutex implements thesync.Locker interface.

typeLockerinterface {Lock()Unlock()}

Cond

Thesync.Cond condition variable can be used to coordinate those goroutines that want to share resources. When the state of shared resources changes, it can be used to notify goroutines blocked by a mutex.

Each Cond has an associated lock (often a*Mutex or*RWMutex), which must be held when changing the condition and when calling the Wait method.

But why do we need it?

One scenario can be when one process is receiving data, and other processes must wait for this process to receive data before they can read the correct data.

If we simply use achannel or mutex, only one process can wait and read the data. There is no way to notify other processes to read the data. Thus, we cansync.Cond to coordinate shared resources.

Usage

sync.Cond comes with the following methods:

  • NewCond(l Locker) returns a new Cond.
  • Broadcast() wakes all goroutines waiting on the condition.
  • Signal() wakes one goroutine waiting on the condition if there is any.
  • Wait() atomically unlocks the underlying mutex lock.

Example

Here is an example that demonstrates the interaction of different goroutines using theCond.

package mainimport ("fmt""sync""time")vardone=falsefuncread(namestring,c*sync.Cond) {c.L.Lock()for!done {c.Wait()}fmt.Println(name,"starts reading")c.L.Unlock()}funcwrite(namestring,c*sync.Cond) {fmt.Println(name,"starts writing")time.Sleep(time.Second)c.L.Lock()done=truec.L.Unlock()fmt.Println(name,"wakes all")c.Broadcast()}funcmain() {varm sync.Mutexcond:=sync.NewCond(&m)goread("Reader 1",cond)goread("Reader 2",cond)goread("Reader 3",cond)write("Writer",cond)time.Sleep(4*time.Second)}
$ go run main.goWriter starts writingWriter wakes allReader 2 starts readingReader 3 starts readingReader 1 starts reading

As we can see, the readers were suspended using theWait method until the writer used theBroadcast method to wake up the process.

Once

Once ensures that only one execution will be carried out even among several goroutines.

Usage

Unlike other primitives,sync.Once only has a single method:

  • Do(f func()) calls the functionfonly once. IfDo is called multiple times, only the first call will invoke the functionf.

Example

This seems pretty straightforward, let's take an example:

package mainimport ("fmt""sync")funcmain() {varcountintincrement:=func() {count++}varonce sync.Oncevarincrements sync.WaitGroupincrements.Add(100)fori:=0;i<100;i++ {gofunc() {deferincrements.Done()once.Do(increment)}()}increments.Wait()fmt.Printf("Count is %d\n",count)}
$ go run main.goCount is 1

As we can see, even when we ran 100 goroutines, the count only got incremented once.

Pool

Pool is s a scalable pool of temporary objects and is also concurrency safe. Any stored value in the pool can be deleted at any time without receiving notification. In addition, under high load, the object pool can be dynamically expanded, and when it is not used or the concurrency is not high, the object pool will shrink.

The key idea is the reuse of objects to avoid repeated creation and destruction, which will affect the performance.

But why do we need it?

Pool's 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. However, it is not suitable for all free lists.

The appropriate use of a Pool is to manage a group of temporary items silently shared among and potentially reused by concurrent independent clients of a package. Pool provides a way to spread the cost of allocation overhead across many clients.

It is important to note that Pool also has its performance cost. It is much slower to usesync.Pool than simple initialization. Also, a Pool must not be copied after first use.

Usage

sync.Pool gives us the following methods:

  • Get() selects an arbitrary item from the Pool, removes it from the Pool, and returns it to the caller.
  • Put(x any) adds the item to the pool.

Example

Now, let's look at an example.

First, we will create a newsync.Pool, where we can optionally specify a function to generate a value when we call,Get, otherwise it will return anil value.

package mainimport ("fmt""sync")typePersonstruct {Namestring}varpool= sync.Pool{New:func()any {fmt.Println("Creating a new person...")return&Person{}},}funcmain() {person:=pool.Get().(*Person)fmt.Println("Get object from sync.Pool for the first time:",person)fmt.Println("Put the object back in the pool")pool.Put(person)person.Name="Gopher"fmt.Println("Set object property name:",person.Name)fmt.Println("Get object from pool again (it's updated):",pool.Get().(*Person))fmt.Println("There is no object in the pool now (new one will be created):",pool.Get().(*Person))}

And if we run this, we'll see an interesting output:

$ go run main.goCreating a new person...Get object from sync.Poolfor the first time:&{}Put the object backin the poolSet object property name: GopherGet object from pool again (it's updated): &{Gopher}Creating a new person...There is no object in the pool now (new one will be created): &{}

Notice how we didtype assertion when we callGet.

It can be seen that thesync.Pool is strictly a temporary object pool, which is suitable for storing some temporary objects that will be shared among goroutines.

Map

Map is like the standardmap[any]any but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes are spread over constant time.

But why do we need it?

The Map type isspecialized. Most code should use a plain Go map instead, with separate locking or coordination, for better type safety and to make it easier to maintain other invariants along with the map content.

The Map type is optimized for two common use cases:

  • 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. In these two cases, the use of async.Map may significantly reduce lock contention compared to a Go map paired with a separateMutex orRWMutex.

The zero Map is empty and ready for use. A Map must not be copied after first use.

Usage

sync.Map gives us the following methods:

  • Delete() deletes the value for a key.
  • Load(key any) returns the value stored in the map for a key, or nil if no value is present.
  • LoadAndDelete(key any) deletes the value for a key, returning the previous value if any. The loaded result reports whether the key was present.
  • LoadOrStore(key, value any) returns the existing value for the key if present. Otherwise, it stores and returns the given value. The loaded result is true if the value was loaded, and false if stored.
  • Store(key, value any) sets the value for a key.
  • Range(f func(key, value any) bool) callsf sequentially for each key and value present in the map. Iff returns false, the range stops the iteration.

Note: Range does not necessarily correspond to any consistent snapshot of the Map's contents.

Example

Let's look at an example. Here, we will launch a bunch of goroutines that will add and retrieve values from our map concurrently.

package mainimport ("fmt""sync")funcmain() {varwg sync.WaitGroupvarm sync.Mapwg.Add(10)fori:=0;i<=4;i++ {gofunc(kint) {v:=fmt.Sprintf("value %v",k)fmt.Println("Writing:",v)m.Store(k,v)wg.Done()}(i)}fori:=0;i<=4;i++ {gofunc(kint) {v,_:=m.Load(k)fmt.Println("Reading: ",v)wg.Done()}(i)}wg.Wait()}

As expected, our store and retrieve operation will be safe for concurrent use.

$ go run main.goReading:<nil>Writing: value 0Writing: value 1Writing: value 2Writing: value 3Writing: value 4Reading: value 0Reading: value 1Reading: value 2Reading: value 3

Atomic

Package atomic provides low-level atomic memory primitives for integers and pointers that are useful for implementing synchronization algorithms.

Usage

atomic package providesseveral functions that do the following 5 operations forint,uint, anduintptr types:

  • Add
  • Load
  • Store
  • Swap
  • Compare and Swap

Example

We won't be able to cover all of the functions here. So, let's take a look at the most commonly used function likeAddInt32 to get an idea.

package mainimport ("fmt""sync""sync/atomic")funcadd(w*sync.WaitGroup,num*int32) {deferw.Done()atomic.AddInt32(num,1)}funcmain() {varnint32=0varwg sync.WaitGroupwg.Add(1000)fori:=0;i<1000;i=i+1 {goadd(&wg,&n)}wg.Wait()fmt.Println("Result:",n)}

Here,atomic.AddInt32 guarantees that the result ofn will be 1000 as the instruction execution of atomic operations cannot be interrupted.

$ go run main.goResult: 1000

Advanced Concurrency Patterns

In this tutorial, we will discuss some advanced concurrency patterns in Go. Often, these patterns are used in combination in the real world.

Generator

generator

Then generator Pattern is used to generate a sequence of values which is used to produce some output.

In our example, we have agenerator function that simply returns a channel from which we can read the values.

This works on the fact thatsends andreceives block until both the sender and receiver are ready. This property allowed us to wait until the next value is requested.

package mainimport"fmt"funcmain() {ch:=generator()fori:=0;i<5;i++ {value:=<-chfmt.Println("Value:",value)}}funcgenerator()<-chanint {ch:=make(chanint)gofunc() {fori:=0; ;i++ {ch<-i}}()returnch}

If we run this, we'll notice that we can consume values that were produced on demand.

$ go run main.goValue: 0Value: 1Value: 2Value: 3Value: 4

This is a similar behavior asyield in JavaScript and Python.

Fan-in

fan-in

The fan-in pattern combines multiple inputs into one single output channel. Basically, we multiplex our inputs.

In our example, we create the inputsi1 andi2 using thegenerateWork function. Then we use ourvariadic functionfanIn to combine values from these inputs to a single output channel from which we can consume values.

Note: order of input will not be guaranteed.

package mainimport ("fmt""sync")funcmain() {i1:=generateWork([]int{0,2,6,8})i2:=generateWork([]int{1,3,5,7})out:=fanIn(i1,i2)forvalue:=rangeout {fmt.Println("Value:",value)}}funcfanIn(inputs...<-chanint)<-chanint {varwg sync.WaitGroupout:=make(chanint)wg.Add(len(inputs))for_,in:=rangeinputs {gofunc(ch<-chanint) {for {value,ok:=<-chif!ok {wg.Done()break}out<-value}}(in)}gofunc() {wg.Wait()close(out)}()returnout}funcgenerateWork(work []int)<-chanint {ch:=make(chanint)gofunc() {deferclose(ch)for_,w:=rangework {ch<-w}}()returnch}
$ go run main.goValue: 0Value: 1Value: 2Value: 6Value: 8Value: 3Value: 5Value: 7

Fan-out

fan-out

Fan-out patterns allow us to essentially split our single input channel into multiple output channels. This is a useful pattern to distribute work items into multiple uniform actors.

In our example, we break the input channel into 4 different output channels. For a dynamic number of outputs, we can merge outputs into a shared"aggregate" channel and useselect.

Note: fan-out pattern is different from pub/sub.

package mainimport"fmt"funcmain() {work:= []int{1,2,3,4,5,6,7,8}in:=generateWork(work)out1:=fanOut(in)out2:=fanOut(in)out3:=fanOut(in)out4:=fanOut(in)forrangework {select {casevalue:=<-out1:fmt.Println("Output 1 got:",value)casevalue:=<-out2:fmt.Println("Output 2 got:",value)casevalue:=<-out3:fmt.Println("Output 3 got:",value)casevalue:=<-out4:fmt.Println("Output 4 got:",value)}}}funcfanOut(in<-chanint)<-chanint {out:=make(chanint)gofunc() {deferclose(out)fordata:=rangein {out<-data}}()returnout}funcgenerateWork(work []int)<-chanint {ch:=make(chanint)gofunc() {deferclose(ch)for_,w:=rangework {ch<-w}}()returnch}

As we can see, our work has been split between multiple goroutines.

$ go run main.goOutput 1 got: 1Output 2 got: 3Output 4 got: 4Output 1 got: 5Output 3 got: 2Output 3 got: 6Output 3 got: 7Output 1 got: 8

Pipeline

pipeline

The pipeline pattern is a series ofstages connected by channels, where each stage is a group of goroutines running the same function.

In each stage, the goroutines:

  • Receive values fromupstream viainbound channels.
  • Perform some function on that data, usually producing new values.
  • Send valuesdownstream viaoutbound channels.

Each stage has any number of inbound and outbound channels, except the first and last stages, which have only outbound or inbound channels, respectively. The first stage is sometimes called thesource orproducer; the last stage is thesink orconsumer.

By using a pipeline, we separate the concerns of each stage, which provides numerous benefits such as:

  • Modify stages independent of one another.
  • Mix and match how stages are combined independently of modifying the stage.

In our example, we have defined three stages,filter,square, andhalf.

package mainimport ("fmt""math")funcmain() {in:=generateWork([]int{0,1,2,3,4,5,6,7,8})out:=filter(in)// Filter odd numbersout=square(out)// Square the inputout=half(out)// Half the inputforvalue:=rangeout {fmt.Println(value)}}funcfilter(in<-chanint)<-chanint {out:=make(chanint)gofunc() {deferclose(out)fori:=rangein {ifi%2==0 {out<-i}}}()returnout}funcsquare(in<-chanint)<-chanint {out:=make(chanint)gofunc() {deferclose(out)fori:=rangein {value:=math.Pow(float64(i),2)out<-int(value)}}()returnout}funchalf(in<-chanint)<-chanint {out:=make(chanint)gofunc() {deferclose(out)fori:=rangein {value:=i/2out<-value}}()returnout}funcgenerateWork(work []int)<-chanint {ch:=make(chanint)gofunc() {deferclose(ch)for_,w:=rangework {ch<-w}}()returnch}

Seem like our input was processed correctly by the pipeline in a concurrent manner.

$ go run main.go0281832

Worker Pool

worker-pool

The worker pool is a really powerful pattern that lets us distributes the work across multiple workers (goroutines) concurrently.

In our example, we have ajobs channel to which we will send our jobs and aresults channel where our workers will send the results once they've finished doing the work.

After that, we can launch our workers concurrently and simply receive the results from theresults channel.

Ideally,totalWorkers should be set toruntime.NumCPU() which gives us the number of logical CPUs usable by the current process.

package mainimport ("fmt""sync")consttotalJobs=4consttotalWorkers=2funcmain() {jobs:=make(chanint,totalJobs)results:=make(chanint,totalJobs)forw:=1;w<=totalWorkers;w++ {goworker(w,jobs,results)}// Send jobsforj:=1;j<=totalJobs;j++ {jobs<-j}close(jobs)// Receive resultsfora:=1;a<=totalJobs;a++ {<-results}close(results)}funcworker(idint,jobs<-chanint,resultschan<-int) {varwg sync.WaitGroupforj:=rangejobs {wg.Add(1)gofunc(jobint) {deferwg.Done()fmt.Printf("Worker %d started job %d\n",id,job)// Do work and send resultresult:=job*2results<-resultfmt.Printf("Worker %d finished job %d\n",id,job)}(j)}wg.Wait()}

As expected, our jobs were distributed among our workers.

$ go run main.goWorker 2 started job 4Worker 2 started job 1Worker 1 started job 3Worker 2 started job 2Worker 2 finished job 1Worker 1 finished job 3Worker 2 finished job 2Worker 2 finished job 4

Queuing

queuing

Queuing pattern allows us to processn number of items at a time.

In our example, we use a buffered channel to simulate a queue behavior. We simply send anempty struct to ourqueue channel and wait for it to be released by the previous process so that we can continue.

This is becausesends to a buffered channel block only when the buffer is full andreceives block when the buffer is empty.

Here, we have total work of 10 items and we have a limit of 2. This means we can process 2 items at a time.

Notice how ourqueue channel is of typestruct{} as an empty struct occupies zero bytes of storage.

package mainimport ("fmt""sync""time")constlimit=2constwork=10funcmain() {varwg sync.WaitGroupfmt.Println("Queue limit:",limit)queue:=make(chanstruct{},limit)wg.Add(work)forw:=1;w<=work;w++ {process(w,queue,&wg)}wg.Wait()close(queue)fmt.Println("Work complete")}funcprocess(workint,queuechanstruct{},wg*sync.WaitGroup) {queue<-struct{}{}gofunc() {deferwg.Done()time.Sleep(1*time.Second)fmt.Println("Processed:",work)<-queue}()}

If we run this, we will notice that it briefly pauses when every 2nd item (which is our limit) is processed as our queue waits to be dequeued.

$ go run main.goQueue limit: 2Processed: 1Processed: 2Processed: 4Processed: 3Processed: 5Processed: 6Processed: 8Processed: 7Processed: 9Processed: 10Workcomplete

Additional patterns

Some additional patterns that might be useful to know:

  • Tee channel
  • Bridge channel
  • Ring buffer channel
  • Bounded parallelism

Context

In concurrent programs, it's often necessary to preempt operations because of timeouts, cancellations, or failure of another portion of the system.

Thecontext package makes it easy to pass request-scoped values, cancellation signals, and deadlines across API boundaries to all the goroutines involved in handling a request.

Types

Let's discuss some core types of thecontext package.

Context

TheContext is aninterface type that is defined as follows:

typeContextinterface {Deadline() (deadline time.Time,okbool)Done()<-chanstruct{}Err()errorValue(keyany)any}

TheContext type has the following methods:

  • Done() <- chan struct{} returns a channel that is closed when the context is canceled or times out. Done may returnnil if the context can never be canceled.
  • Deadline() (deadline time.Time, ok bool) returns the time when the context will be canceled or timed out. Deadline returnsok asfalse when no deadline is set.
  • Err() error returns an error that explains why the Done channel was closed. If Done is not closed yet, it returnsnil.
  • Value(key any) any returns the value associated with the key ornil if none.

CancelFunc

ACancelFunc tells an operation to abandon its work and it does not wait for the work to stop. If it is called by multiple goroutines simultaneously, after the first call, subsequent calls to aCancelFunc does nothing.

typeCancelFuncfunc()

Usage

Let's discuss functions that are exposed by thecontext package:

Background

Background returns a non-nil, emptyContext. It is never canceled, has no values, and has no deadline.

It is typically used by the main function, initialization, and tests, and as the top-level Context for incoming requests.

funcBackground()Context

TODO

Similar to theBackground functionTODO function also returns a non-nil, emptyContext.

However, it should only be used when we are not sure what context to use or if the function has not been updated to receive a context. This means we plan to add context to the function in the future.

funcTODO()Context

WithValue

This function takes in a context and returns a derived context where the valueval is associated withkey and flows through the context tree with the context.

This means that once you get a context with value, any context that derives from this gets this value.

It is not recommended to pass in critical parameters using context values, instead, functions should accept those values in the signature making it explicit.

funcWithValue(parentContext,key,valany)Context

Example

Let's take a simple example to see how we can add a key-value pair to the context.

package mainimport ("context""fmt")funcmain() {processID:="abc-xyz"ctx:=context.Background()ctx=context.WithValue(ctx,"processID",processID)ProcessRequest(ctx)}funcProcessRequest(ctx context.Context) {value:=ctx.Value("processID")fmt.Printf("Processing ID: %v",value)}

And if we run this, we'll see theprocessID being passed via our context.

$ go run main.goProcessing ID: abc-xyz

WithCancel

This function creates a new context from the parent context and derived context and the cancel function. The parent can be acontext.Background or a context that was passed into the function.

Canceling this context releases resources associated with it, so the code should call cancel as soon as the operations running in this context is completed.

Passing around thecancel function is not recommended as it may lead to unexpected behavior.

funcWithCancel(parentContext) (ctxContext,cancelCancelFunc)

WithDeadline

This function returns a derived context from its parent that gets canceled when the deadline exceeds or the cancel function is called.

For example, we can create a context that will automatically get canceled at a certain time in the future and pass that around in child functions. When that context gets canceled because of the deadline running out, all the functions that got the context gets notified to stop work and return.

funcWithDeadline(parentContext,d time.Time) (Context,CancelFunc)

WithTimeout

This function is just a wrapper around theWithDeadline function with the added timeout.

funcWithTimeout(parentContext,timeout time.Duration) (Context,CancelFunc) {returnWithDeadline(parent,time.Now().Add(timeout))}

Example

Let's look at an example to solidify our understanding of the context.

In the example below, we have a simple HTTP server that handles a request.

package mainimport ("fmt""net/http""time")funchandleRequest(w http.ResponseWriter,req*http.Request) {fmt.Println("Handler started")context:=req.Context()select {// Simulating some work by the server, waits 5 seconds and then responds.case<-time.After(5*time.Second):fmt.Fprintf(w,"Response from the server")// Handling request cancellationcase<-context.Done():err:=context.Err()fmt.Println("Error:",err)}fmt.Println("Handler complete")}funcmain() {http.HandleFunc("/request",handleRequest)fmt.Println("Server is running...")http.ListenAndServe(":4000",nil)}

Let's open two terminals. In terminal one we'll run our example.

$ go run main.goServer is running...Handler startedHandlercomplete

In the second terminal, we will simply make a request to our server. And if we wait for 5 seconds, we get a response back.

$ curl localhost:4000/requestResponse from the server

Now, let's see what happens if we cancel the request before it completes.

Note: we can usectrl + c to cancel the request midway.

$ curl localhost:4000/request^C

And as we can see, we're able to detect the cancellation of the request because of the request context.

$ go run main.goServer is running...Handler startedError: context canceledHandlercomplete

I'm sure you can already see how this can be immensely useful.

For example, we can use this to cancel any resource-intensive work if it's no longer needed or has exceeded the deadline or a timeout.

Next Steps

Congratulations, you've finished the course!

Now that you know the fundamentals of Go, here are some additional things for you to try:

I hope this course was a great learning experience. I would love to hear feedback from you.

Wishing you all the best for further learning!

References

Here are the resources that were referenced while creating this course.

About

Master the fundamentals and advanced features of the Go programming language

Topics

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Sponsor this project

    Contributors6

    Languages


    [8]ページ先頭

    ©2009-2025 Movatter.jp