README
¶
Let's Go workshop
Introduction
This workshop aims to give an introduction of the Go language by writing code and practising the language. Step by step more code will be introduced and the final product will be a tool that creates GIFs from an input text.
Tools needed for the workshop
- Go
- Download at go.dev
- Verify that it's working by running
go version
in the command line
- The IDE of your choice
- Visual Studio Code with the Go extension
- Goland (from JetBrains)
- Vim plugin https://github.com/fatih/vim-go
- Git
- To commit the different milestones
- pkg.go.dev
- This is the link to the documentation of the various Go packages available
- Inside or outside the standard library
- Useful to learn about each symbol (types, functions, methods, and so on) and what they do for each package. It usually contains examples of usages of these symbols as well
- This is the link to the documentation of the various Go packages available
Milestones
Here is the list of different milestones we are going to acheive during the workshop.
Hello, World! (milestone 0)
For the first step you are going to write the hello world, and this is also going to be the occasion to setup your Go project.
Steps
- Create a folder, e.g. lets-go-workshop; This will contain the code to write for the workshop
- Go inside the folder and run
go mod init
from the terminal - (optional) run
git init
if you want to store the code for the different milestones in a different commit - Open your IDE inside the workshop folder
- Create a
main.go
file - Write the following code:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
- Run
go run main.go
Explanations
The command go mod init
initializes a project making it a Go module. A Go module represents a set of Go files that are supposed to be shipped altogether: e.g. an application or a library.
Every Go application starts at a main()
function located in a main.go
file.
Every Go file resides in a package
, this information is declared as the first instruction of the file. The main function should reside in package main.
Packages can contain functions, constants, custom types and so on inside a specific namespace; to use a specific package it needs to be imported with the import
keyword. An unused import results in a compilation error.
Any exported/public symbol inside a package can be invoked in the form of package name.symbol name
. For example, in the call fmt.Println
, fmt is the package name and Println is the symbol name.
In Go ;
is not mandatory at the end of each statement.
Milestone 1
For this step you are going to improve on the hello world with some code that creates a file and writes the hello world on it. The goal is to see variable declaration and initialization, conditionals with the if statement, to start interacting with some packages the standard library and to introduce few Go specific features: the blank identifier, the multi-value returns, the error type and the defer keyword.
Steps for milestone 1
- Create a file (use the
os
package), write the "hello, world!" string to the file (use thefmt
package), close the file - if there are any errors during the file creation, log them and exit (use the
log
package) - Use the defer keyword to defer the execution of the Close() call
- Use the command line arguments to take the text to print (use the
flag
package) - Run
go run main.go "Hello, World!"
Notes for milestone 1
Use pkg.go.dev to search the content of the packages os
, fmt
, log
and flag
.
To declare and assign variables you can use two tools:
- The
:=
operator (declare and assign)- a := 1
- The
var
keyword (declare), followed potentially by the=
operator (assign)- var a int = 1
When the compiler can infer the type of a variable it can be omitted:
- var a int = 1 // => var a = 1
- a := 1
- in this statement the type is already omitted
When the type name is present in a statement, it always goes after the name:
- var a int // 👍
- var int a // ❌ (compilation error)
You can declare and/or assign multiple variables, of the same or different type, at the same time:
- a, b, c := "hello", 1, "world"
- f, err := os.Create("myFile") // os.Create is a function that returns 2 values
If you don't need a specific value during an assignment you can ignore it using the blank identifier _
. Any unused variables are considered complier errors.
- f, _ := os.Create("myFile") // following the previous example, I just need the value of f from the function
The if statement is similar to most languages, here are some differences:
- No
()
are required around the condition {}
are always required around the code to execute if the condition is true, even if it fits in one line
The defer keyword instructs the function that follows to be called at the end of the function where it is deferred.
Generally, in a multi-value return the second argument or last argument is of type error which is an interface that declares one method: func Error() string
A value of a specific interface type can be compared only with another value of the same type or with nil
which means: no value for that interface.
Milestone 2
In this milestone you'll create your first image (a PNG); in doing so you'll learn about creating your own package and function, invoke them and learn about for loops, types, zero values and the meaning of exported and unexported.
Steps for milestone 2
- Create a new folder named myimage; in Go, this is a new package
- Inside the myimage folder create a new file called myimage.go
- Write a function
func New(l, h int, c color.RGBA) *image.Paletted
inside myimage.go that draws a rectangle of the specified size and color (use theimage
package, see functionsRect
,NewPaletted
and theSet
method of thePaletted
type) - Change the main.go to first call the function that draws the rectangle then use
png.Encode
to save the rectangle image in a PNG file - Run
go run main.go
with the appropriate parameter if you read them from the console
As a bonus you can add more input flags for the properties of the rectangle (length, height and color) and the output file name.
Notes for milestone 2
A package name and its containing folder do not have to have matching names, but it's good practice to do so.
There are no specific keywords to define the scope of a Go symbol:
- Name is UPPERCASE
- Symbol is exported, which means available to all packages (public-level scope)
- Name is lowercase
- Symbol is unexported, which means available to the package where it is located (package-level scope)
There three kind of types in Go:
- Primitives
- Internal types
- Custom types
Primitives in Go are bool (true or false), the literal type string and numeric types rune, byte (rune and byte can also represent literals), uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64, complex64, complex128).
Internal types are array, function, slice, map, and channel.
Custom types are all Go types declared with the type
and/or struct
keywords.
For example:
type Age int
type Person struct {
name string
age Age
}
Every variable is initialized in Go at the declaration time; if not explicitly initialized in the code, they will be initialized by Go with the zero value for the type of that variable.
Here is the zero value list:
var (
b bool // false
i int // 0 (same for all numeric types)
s string // ""
p Person // Person{}, all custom types are initialized with all fields set to their zero values
v map[int]string // nil, also valid for all internal types
)
Looping in Go is similar to most languages with some differences:
for
is the only keyword used for loops- No
()
are required around the loop instructions {}
are always required around the code block to execute inside the loop, even if it fits in one line- for condition {} replaces the while statement; there are no do/while statements in Go
- for true {} represents an infinite loop (to be interrupted with a
break
statement
Using flags in Go requires the usage of the flag
package:
- first define all flags using the functions
Int
,String
,Bool
and so on, according to the type of the data you request in input- They all take flag name, default value and short description as inputs and return pointers of the same type as in the function name
- They can be visualized when running
go run main.go --help
, without needing to define a flag for the help message
- then call the function
flag.Parse()
to parse all flags defined above
Pointers in Go are similar to C/C++. Use *
declare or dereference them, use &
to take the address of a value.
Milestone 3
In this milestone you'll write your first message on the previously created image; in doing so you'll learn interacting with external packages from the standard library and see how to import them in your project.
Steps for milestone 3
- Write a function
func Write(dst draw.Image, text string, c color.RGBA, fontPath string, fontSize float64) error
inside myimage.go that draws a text on the rectangle created in milestone 2 - Import the package
github.com/golang/freetype
inside myimage.go - In the terminal, at the root of the project run
go mod tidy
- Write the code to create and configure the
freetype.Context
object - Call the
Write
function inside the main function - Run
go run main.go
with the appropriate parameter if you read them from the console
As a bonus you can:
- keep adding input flags for the color of the string, the font file path and the font size.
- create a function that gives the appropriate size for the rectangle by taking into account the font size and the length of the text to draw
Notes for milestone 3
The difference between packages from the standard library and the external ones can be seen from its naming when getting or importing them. In fact, the main difference is that external packages include a complete path to external package containing:
- the registry: github, gitlab, bitbucket, and so on
- the package author: be it a username or a project group name
- the package name and path to subpackage if relevant
- in local it will be the path to the package
the function Join
of the strings
package is a useful tool to join several strings, organized into a slice (array), in a single string.
Milestone 4
In this milestone you'll transform the previously created image into a blinking gif that alternates between two frames that switch their colors; by working on this milestone you'll learn to work with slices (mostly known as resizable arrays), the switch and for range statements, the make and append keywords and to interact with constants and interfaces.
Steps for milestone 4
- Create a new folder in the project root named mygif
- Inside the mygif folder create a new file called mygif.go
- Write a function
func New(text string, c1, c2 color.RGBA, fontPath string, fontSize float64) ([]*image.Paletted, error)
inside mygif.go that creates a slice containing the frames to be stored in the gif and an error if something goes wrong (use themyimage
package created in the previous milestones) - Write another function
func Save(w io.Writer, frames []*image.Paletted, delay int) error
inside mygif.go that stores the frames created in the New function into a gif file (use thegif
package and don't forget to set the Delay field for the gif to store) - Call the
New
andSave
functions inside the main function - Run
go run main.go
with the appropriate parameter if you read them from the console
As a bonus you can:
- derive the name of the output file from the input text
- create a custom type
GIF
inside mygif.go to hold the frames and the delay information and make theNew
function returnGIF
instead of[]*image.Paletted
, transform theSave
function into a method of the GIF structure and adapt the calls in the main function to accomodate these changes
Notes for milestone 4
Constants in Go are declared with the const
keyword followed by name = value
. The value can only be hardcoded in constants or be taken from other constants.
Slices and arrays in Go can be associated with arrays from other programming languages. The main differences are:
- Arrays have fixed length
var array [2]int // array of int with length 2
- Slice have variable length
var elems []int // slice of int with length 0
- Can initialize a slice (not an array) with the
make
built-in functionelems = make([]int, 2) // {0, 0}
- Can add elements to the slice with append built-in function
elems = append(elems, 4) // {0, 0, 4}
- Arrays and slices can sliced to take a subset of their content
numbers := []int{0, 1, 2, 3}
numbers[1:3] // {1,2}
numbers[1:] // {1,2,3}
numbers[:2] // {0,1}
The switch statement is similar to most languages, here are some notable features:
- No
()
are required around the condition or value to switch on {}
are always required around the code block to execute inside the switch- No
break
statement is necessary after each case as the execution will exit the switch statement after the selectedcase
ordefault
statement is completed Fallthrough
is the keyword to use before the case statement completes if you want to keep checking other cases- A plain switch, e.g.
switch {}
is generally used instead of an if/else block
Continuing on the loops in Go, the for range statement is used on symbols that can be looped upon, the syntax of the for range is the following:
for i, e := range numbers {
fmt.Println(i, e)
}
When looping on:
- strings,
i
is the index ande
is the character at position i - arrays and structs,
i
is the index ande
is the value (e.g. the value of number[i]) - maps,
i
is the key of the map ande
is the value corresponding to that key
Interfaces in Go are methods sets that are declared with the type
and interface
keywords.
type Stringer interface {
String() string
}
To implement an interface a (custom) type has just to implement the methods that are listed in its method set. There are no keywords like implements or extends like in other languages.
type Person struct {
name string
age int
}
func (p Person) String() string { // Person now implements the Stringer interface
return fmt.Sprintf("%s,%d", p.name, p.age)
}
The (p Person)
between the func
keyword and the name of the function is called the receiver type and it means that function becomes a method attacched to the type Person.
Documentation
¶
There is no documentation for this package.