PACKAGE - is collection of related Go source files that are organised together. Each file in a package shares the same package name. Only one package is allowed in a folder!
MODULE - defines a project name. It consists of a collection of related packages, that are versioned together as a single unit.
You create a new Project/Module in a folder with a command:
$ go mod init github.com/username/yourproject
This Module's folder can have any number of nested folders, each representing a package. For example, package utils:
% tree . ├── go.mod ├── main.go └── utils # 'utils' is a package inside 'yourproject' module. └── math.go
And the code in math.go:
package utils func Add(a, b float64) float64 { return a + b }
Now, calling that Add function from the main.go in the module's folder:
package main import ( "fmt" "github.com/username/yourproject/utils" ) func main() { r := utils.Add(2, 3) fmt.Println(r) }
$ gofmt -d calculator.go # just shows a diff $ gofmt -w calculator.go # rewrites the file
Each test in a Go project is a function. In order for a function to become a “test function” there are couple of requirements:
Running a test for a package:
$ go test $ go test -count 100 # repeat a test 100 times
Guideline:
Test Coverage
% go test -cover % go test -coverprofile=coverage.out # generate coverage profile % go tool cover -html=coverage.out # inspect it in the browser
Go has a type to communicate errors - error. Example usage:
func (book *Book) SetCopies(copies int) error { if copies < 0 { return fmt.Errorf("negative number of copies: %d", copies) } book.Copies = copies return nil }
err := book.SetCopies(-1) if err != nil { fmt.Println("Oh dear, something went wrong:", err) }
Go assigns a default zero value to any variable, that is declared but not assigned a value.
Style guide
If we declare a variable, and would like its starting value to be zero, we do like this:
// declaring with a zero value var x int
If you want a starting value to be something else - use short declaration :=:
// declaring with a specific value y := 4
Slice can be declared as a nil or as an empty slice:
var b = []Book // nil slice, declared but not initialised var b = []Book{} // empty slice, initialised with 0 elements, points to the empty array. Slice literal
== operator isn't defined for Slices! We can use slices.Equal function to compare slices:
import "slices" if !slices.Equal(want, got) { t.Fatalf("want: %q, got: %q", want, got) }
Sorting a Slice of Struct
type Person struct { Name string, Age int, } people := []Person{ {"Alice", 25}, {"Bob", 30}, } slices.SortFunc(people, func(a,b Person) int { // slice.SortFunc - generic function for sorting slices return cmp.Compare(a.Age, b.Age) // with a custom comparison. }) // cmp.Compare - a helper that returns -1, 0, or 1
Map is a reference type - when we pass it to the function, we are passing a reference, not a copy.
Map syntax #1:
colors := map[string]string{ "red": "#ff0000", "green": "#008000", }
Map syntax #2:
colors := make(map[string]string) // Creates an empty map colors["red"] = "#ff0000" // Add key/value to it delete(colors, "red") // Deleting a key
Iterating over a map:
func printMap(c map[string]string) { for key, value := range c { fmt.Println(key, value) } }
Go doesn't allow to update the field of map elements directly! Instead, you have to use a temp variable:
// Doesn't work! catalog[1].Title = "New Title" // Use temp variable instead b := catalog[1] b.Title = "New Title" catalog[1] = b
Retrieving non-existing elements.
An interesting property of Go maps is that looking up a non‐existent key doesn’t cause an error: instead, it returns the zero value of the element type.
Get map's values:
import ( "maps" "slices" ) books := maps.Values(catalog) // returns an Iterator books_slice := slices.Collect(books) // "collects" all elements from the Iterator into the Slice
Checking for a missing value:
book, ok := catalog["some-key]"] // maps support "comma, ok" pattern. ok == false if key is missing
Empty literal:
b := Book{}
Defining a method - similar to defining a function, but it also has a receiver - that represents the object that the method is called on.
It's not possible to add methods to the existing type. E.g. following doesn't work:
type MyB strings.Builder func TestMyB(t *testing.T) { var mb mytypes.MyB mb.WriteString("Hello") // FAILS! We can't access strings.Builder's methods if we define } // our type based on it
Instead, we can create a struct type, with a field of type we want.
Sometimes we need to know whether a function succeeded - for example, whether a search found anything. We can design the function to return two values: the result and a boolean indicating success.
func GetBook(id string) (Book, bool) { for _, book := range catalog { if book.ID == id { return book, true } } return Book{}, false }
x := 5 y := &x // `y` is a pointer to `x`. `&` is an address operator fmt.Println(*y) // *y dereferences y - it retrieves the value that y “points” to
In order to create a Method, we need to specify the receiver parameter. It tells Go that this method should be called on a specific object value, and that value is available inside the method:
func (book Book) String() string { // (book Book) - method's receiver return fmt.Sprintf("%v by %v (copies: %v)", book.Title, book.Author, book.Copies) }
There are two types of receivers - Value and Pointer receivers. Think of it as how the function gets access to the object:
func (book Book) SetCopies(copies int) { book.Copies = copies // only affects local `book`, not the original }
And the pointer example:
func (book *Book) SetCopies(copies int) { book.Copies = copies // Go provides an automatic de-referencing when we use // pointers to struct. Pointers can't have fields, so there // is no ambiguity here. Otherwise it would look like: // (*book).Copies = copies } book := books.Book{ Copies: 5, } book.SetCopies(12) // That's here, when we pass `value` or `reference` to // the method