Go - kamialie/knowledge_corner GitHub Wiki
- Data types
- Code structure
- Program flow
- Object orientation and polymorphism
- Standard library
- CLI
- IDE
- Frameworks
- Resources
Hello world:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello world!")
}
Language characteristics:
- Strong, static type system
- C-inspired syntax
- Multi-paradigm (procedural and object-oriented)
- Garbage collector
- Fully compiled
- Rapid compilation
- Single binary (however, intermediate libraries, plug-in systems are available)
Use cases:
- Web services (moving data between servers)
- Web applications (end user, html, etc)
- DevOps space
- GUI
- Machine learning
Semi-colons are automatically added by compiler to the end of lines that do not end with a symbol.
Go is statically-typed language. Variables can be declared explicitly or use implicit initialization syntax (compiler determines the type).
Go builtin - about simple types and more.
Declaring a variable:
package main
func main() {
// Verbose
var i int
i = 42
// One line
var f float32 = 3.14
// Implicit initialization syntax
var name = "Mike"
// Short declaration syntax ":="
firstName := "Name"
// Complex type
c := complex(3, 4)
// Split real and imaginary parts; similarly declare multiple variables at
// once
r, i := real(c), imag(c)
a, b := 3, 5
// Variable block (does not require implicit declaration syntax `:=`):
var(
index = 1
)
}
Dividing integers always is performed as integer division (remainder is dropped); the result of a floating number division is a decimal number.
String types:
- "example" (with quotes) - interpreted string, handles escape characters as well
- `example` (backticks) - raw string
Numeric types include integers, unsigned integers, floating point numbers, and complex numbers. Common examples are int, uint, float32 and float64, complex64, complex128 respectively.
Simply true
or false
value.
Conventional interface for representing an error condition, with the nil value representing no error. Represents almost any type or data type that has an error method, meaning it can report what the actual error was.
Uninitialized pointer equals nil
. Value can not be assigned through
uninitialized pointer, thus, it should be initialize with new()
function
or getting an address of a pre-existing variable.
package main
import "fmt"
func main() {
var firstName *string = new(string)
firstName = "Name"
foo := "bar"
ptr := &foo
*ptr = "updated_bar"
fmt.Println(ptr, *ptr)
}
Constant must be declared at the time as initialization (same line). Value type
must be known at compile time. Implicitly typed constant (type is reevaluated
for each case) can be also declared on a package level (outside of any
function): either standalone or as const
block.
Implicitly typed declaration marks a variable as a literal, which allows it to be used in other contexts, e.g. assigning integer constant to floating point variable type. Explicitly typed constant enforces that variable to be used only when that specific type is allowed to be used.
Uninitialized constant gets the constant expression of a previous constant, e.g. 2 * 5.
package main
import "fmt"
const(
foo = "bar"
b // "bar"
)
const pi = 3.14
func main() {
const pi float32 = 3.14
const c = 3
fmt.Println(c + 2)
fmt.Println(c + 1.5)
}
Used only in the context of constant variables. Represents an expression, not a
literal value, which gets incremented on position within a block. Can also be
used in expressions. The value of iota
resets with a new constant block.
package main
const(
a = iota // 0
b // 1
c = 3 * iota // 6
)
const(
d = iota // 0
)
const(
e = 5
f = iota // 1
)
Fixed sized collection. All items must be of the same type. 2 arrays can be
compared, using ==
operator, - first, length and type are compared, then each
item is compared for equality.
var arr [3]int
arr[0] = 1
implicit_arr := [3]int{1, 2, 3}
fmt.Println(len(arr))
// Arrays are copied by value
// arr2 is a distinct array with exactly same value
arr2 := arr
Dynamically sized collection (reference type). Doesn't actually hold its own data, but rather refers to underlying array (if data changes in that array, it is also reflected in the slice as well). 2 slices can not be compared.
// As opposed to array, no need to specify size
slice := []int{1, 2, 3}
// Add elements to the slice, put slice itself as first argument
slice = append(slice, 4)
// Remove elements from the slice; remove indices 1 and 2
// Requires experimental slices package as of 1.19
slice = slices.Delete(slice, 1, 3)
# starting from 1st element
s2 := slice[1:]
# up to but not including
s3 := slice[:2]
# starting and ending indexes
s4 := slice[1:2]
arr := [3]int{1, 2, 3}
# slice operator
# any changes to array are reflected in a slice and vice versa
# slice is sort of pointing to the array
slice := arr[:]
Similar to slice, map is a reference type - actual data is stored elsewhere. 2 maps can not be compared.
# Key of type string, value of type int
# All keys and all values should be of the same type as defined
m := map[string]int{"foo": 42}
fmt.Println(m["foo"])
// Update existing item or create new
m["bar"] = 21
delete(m, "foo")
Queries always return a result; for non-existing key the default value for the
type is returned, e.g. 0 for int. Comma ok syntax can be used to determine,
if a value came from the map or not. ok
is just a conventional name for
variable in this context; is of type boolean.
v, ok := m["foo"]
Fields can be any of types, but are fixed at compile time. Declaration and definition must be separate. Each field is initialized to zero value. Struct is a value type, like arrays, which means assignment operation creates a separate value copy.
// Anonymous structure
var s struct {
name string
}
// Custom structure
type user struct {
ID int
firstName string
lastName string
}
var u user
u.ID = 1
u.firstName = "Alex"
u2 := user{ID: 1, firstName: "Alex", lastName: "Dot"}
Function parameters can stay unused - this won't cause compile error. Parameters of the same type can be listed under single type as a list.
func function_name(param1 int, param2 string) {
}
func function_name(param1, param2 int) {
}
func function_name(param1, param2 int) bool {
return true
}
Variadic parameter transforms the type to a collection. Within a function acts as a slice. Can only specify one variadic parameter and can only be specified last. Function call accepts multiple values separated by comma.
func print_names(names ...string) {
for _, n : range names {
println(n)
}
}
// print_names("Alex", "Max", "Sam")
Return value is optionally set. Multiple value can be returned. Common practice is to return a value alongside error (pointer type) in case function encounters a problem. To ignore a return value use write only variable. import "errors"
func function_name(param1, param2 int) error {
return errors.New("smth went wrong")
}
func function_name(param1, param2 int) (int, error) {
return 1, nill
}
// write only variable
_, err = function_name()
Return values can also be named; in this case also a naked return statement can be used, normal return statement can still be used as well (rarely used):
func function_name(param1, param2 int) (result int, status error) {
result := 1
var status error = nil
return
}
A package is a directory located in the same space as go.mod
, which
contains collection of source files that are compiled together (at least one
file). Packages can also be nested - another directory within a package that
also has at least one source file is also a package (subpackage).
Any source file starts with a package <value>
statement, package
declaration. Either main
or name of directory that contains the package.
Functions, types, variables, and constants defined in sources files are members of a package.
There are 2 visibility modes: package and public. Visibility mode determines
what importing code can see from a package. Package mode makes all members
visible within a package by default. Capitalized members, e.g. type User struct
have a public visibility, become part of public API of a package.
Public members are visible for in imported packages. Same applies to structure
members.
Name of directory becomes the name of the package. Package can be referenced from the root package by appending it to path after slash.
Fully Qualified Package Name is used for imports; check also identifiers.
To import a bar
package in the root package in a module named foo
(look in
go.mod
file) the following identified is used - foo/bar
.
A module is a collection of related Go packages that are released together.
Comments follow C notation:
// single line comment
var i // same line comment
/*
multiline comment
*/
Documentation is expected in certain places of any Go program. First, explain what package provides and what it is used for. Start with Package .
// Package user provides ...
package user
Any public members of a package are also expected to be documented, such as variables and functions. Start with the name of a member, e.g. GetByID searches...:
// MaxUsers controls how many user can be handled at once.
const MaxUsers = 100
// GetByID searches for users using their employee ID.
func GetByID(id int) (User, bool) {}
If statements. Initilizer can also be optionally specified; valid only on first if clause, if multiple branches are also specified.
i := 1
j := 2
if i == j {
println("equal")
} else if i < 2 {
println("less")
} else {
println("more")
}
if k := 1, k == 1 {
println("if with initializer")
}
Switch statements. Break is implicit. Multiple case expression can be specified. Initializer syntax can be used here as well, similarly to if statements.
method := "POST"
switch method {
case "POST":
println("POST")
case "GET", var:
println("GET")
fallthrough
case "PUT":
println("PUT")
default:
println("default")
}
Logical switch is the one that has test expression set to true
, so each case
checks if it evaluates to true
. Since this is a common use case, true
can
be omitted, in which case it is implied.
switch i := 2, true {
case i < 5:
println("i is less that 5")
case i < 10:
println("i is less that 10")
default:
println("i is greater or equal to 10")
}
Basic loops:
- infinite loop
var i int for { if i == 5 { break } println(i) i += 1 }
- loop till condition
var i int for i < 5 { if i == 1 { continue } if i == 3 { break } i += 1 }
- loop till condition with post clause
for i := 0; i < 5; i++ { ... }
Looping over collections structure follows the form
for key, value := range collection { ... }
. For slice and array a key is an
index.
slice := []int{1, 2, 3}
// range keyword tells that the following identifier represents
// a collection, and returns key/value pair on each iteration
for i, v := range slice {
println(i, v)
}
m := map[string]int{"one": 1, "two": 2}
for k, v := range m {
println(k, v)
}
# Retrieve only keys
for k := range m {
println(k
}
# Retrieve only values using blank identifier
for _, v := range m {
println(v
}
Perform additional statements after function exit and before returning focus to
the called. Deferred statements are identified by defer
keyword, and are
executed in the opposite order - last one registered is executed first.
func main() {
println("main 1")
defer println("defer 1")
println("main 2")
defer println("defer 2")
}
/*
main 1
main 1
defer 2
defer 1
*/
panic
keyword immediately stops the execution of the current function and
returns focus to the called. If no recovery steps are present, this repeats
until program ultimately exits. Any type of data can be passed to a panic
function, e.g. string, slice, object.
func example() {
println("One")
panic("Error message")
println("Two")
}
Recover is done with defer
concept and recover
function. The latter
returns everything that was passed to panic
. Once recover
function is
called, Go assumes that panic was handled and continues normal execution
of the calling function.
func example() {
defer func() {
fmt.Prinln(recover())
}()
fmt.Prinln("One")
panic("Error message")
fmt.Prinln("Two")
}
Anonymous function executes even if no panic occurred. return
function
return nil
, if panic has no occurred, which can be used in an if statement
to perform recovery steps or not.
func example() {
i := 10
if i < 15 {
goto my_label
}
my_label:
j := 42
for ; i < 15; i++ {
...
}
}
goto
statement in Go follows 4 rules:
- Can leave a block, e.g. if statement, for loop
- Can jump to a containing block, e.g. function
- Can no jump after variable declaration - if variable was not declared as part of normal sequential execution, it will not be known.
- Can not jump directly into another block, e.g. any block at the same level as the block that was left.
Multiple functions can be grouped via struct, thus, becoming methods. Method
needs a type to bind to. A method receiver is between func
and name of the
method. Thus, methods represents a tight coupling between a function and a
type - method only works in the context of a specific type.
type someStruct struct {}
func (ss someStruct) func_name(param1 int, param2 string) {}
var newStruct someStruct
result = newStruct.func_name(35, "string")
Receiver could be of type value or pointer. When data has to be shared between the caller and a method, a pointer should be used.
type user struct {
id int
name string
}
func (u user) String() string {
return fmt.Sprintf(..)
}
func (u *user) UpdateName(n name) {
u.name = name
}
Similar to struct, but contains methods that must be implemented. Once an interface variable is created, and particular type is assigned to it, only interface methods are visible.
type Reader interface {
Read([]byte) (int, error)
}
type File struct {...}
func (f File) Read(b []byte) (n int, err error)
type TCPConn struct {...}
func (t TCPConn) Read(b []byte) (n int, err error)
var f File
var t TCPConn
var r Reader
r = f
r.Read(...)
r = t
r.Read(...)
var f File
var r Reader = f
// A compile time error, since Go doesn't know the underlying type of an
// interface in general.
var f2 File = r
// Type assertion - tell explicitly what type does the variable contain. If
wrong, a runtime panic occurs.
f2 = r.(File)
// Safe type assertion
f2, ok := r.(File)
To test an interface for multiple underlying types use type switch:
var f File
var r Reader = f
switch v := r.(type) {
case File:
// v is now a File object
case TCPConn:
// v is now a TCPConn object
default:
// if v is none of the objects types above
}
Interface loses the original identity of an object, thus, requiring type assertion to be able to access all other properties of a given object. Generic function allows to temporary treat an object to act as another type and return to its original form outside this function.
Generic type is defined after a function name, but before parenthesis for
parameter list. any
is a built-in constraint, which represents an interface
with 0 or more methods.
//Accepts any time of slice
func clone[V any](s []V) []V { ... }
For generic function that deals with maps any
can not be used for the key, as
it has to be some type that can be compared; comparable
represent such types.
//
func clone[K comparable, V any](m map[K]V) map[K]V { ... }
Some operations, such as addition, can not be performed for all types, any
.
Thus, an explicit interface must be created, where each type is listed, to
allow Go to check, if that type can performed certain actions. Adding boolean
to the interface will cause an error.
type addable interface {
int | float64
}
func add[V ](s []V) V {
var result V
...
result += V
}
Constraints package contains more types that are not built-in.
net and net/http
package main
import(
"io"
"net/http"
"os"
)
func main() {
http.HandleFunc("/", Handler)
http.ListenAndServe(":3000", nil)
func Handler(w http.ResponseWriter, r *http.Request) {
f, _ := io.Open("./file.txt")
io.Copy(w, f)
}
}
os, fmt, bufio
package main
import(
"bufio"
"fmt"
)
func main() {
fmt.Println("Enter text")
// Wrap raw Stdin with reader decorator
in := bufio.NewReader(os.Stdin)
}
var = flag.String(name, default_value, description)
flag.Parse()
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
})
err := http.ListenAndServe(":3000", nil)
if err != nil {
log.Fatal(err)
}
}
# initialise project
$ go mod init
# run single file
$ go run main.go
go env - prints available env vars, can also modify, unset and add more go fmt (calls gofmt standalone) - applies best practices for formatting source code.
go get - get (download and install) packages (also modifies go.mod) go list -m all - list all packages
go doc - prints the documentation of the package or symbols inside it
Go run
command builds and runs an application in place without leaving a
binary on disk. Provide path to the source file as a command parameter.
Imported dependencies must be included in the
Go build
compiles the packages - binaries are saved, but libraries are
discarded by default. However, this behavior can be changed with various flags.
# save library after build
$ go build -a package_name.a package_name
# save intermediate dependecies (pkg directory)
$ go build -i binary_name
VSCode
Shift + CMD + P -> Go: Install/Update Tools (needs gopls first - language server)
- go mod init
To be able to pass input to the program and debug it add the following to launch configuration:
{
"configurations": [
"console": "integratedTerminal"
]
}
Go kit - comprehensive microservice framework
Gin - fast, lightweight web framework
Gorilla Toolkit - collection of useful tools
Cobra - framework for building CLI apps