Short basic introduction to Go v1.18 programming language
Go Logo is Copyright 2018 The Go Authors. All rights reserved.
This work was written because it is said everywhere that Go is small and quick to learn, but I could not find any introduction that reflected this for me. I have therefore collected and reorganized the most essential basic information, somewhat as it is presented in The Specification itself, similar to a Tutorial, but much less verbose and convoluted, somewhere between the Examples and Specifications genre. I recommend trying out all the examples on Playground, or even better, in your own development environment, to better absorb the knowledge. Many examples or good descriptive passages are taken from the great referenced works below, as I could not think of a better one, nor did I want to surpass them.
- Comments
- Literals
- Identifiers
- If Conditional Execution
- Switch Multi-way Execution
- For The Loop
- Packages
- Import Packages
- Goroutines
- Used and proposed sources
- Author
- Support
- License
Comments serve as program documentation.
// Package main provides possibility to see Go documentation.
package main
import "fmt"
// Version represents the version number of the package.
const Version = 1.0
// Help returns nothing, but prints the usage as side effect.
func Help() {
fmt.Println("My main package. Created by me.")
fmt.Printf("Version: %.1f\n", Version)
}
func main() {
/*
General comments start with the character sequence slash-star and stop with
the first subsequent character sequence star-slash.
To document a type, variable, constant, function, or even a package, write
a regular comment directly preceding its declaration, with no intervening
blank line.
*/
// Line comments start with the character sequence slash-slash and stop at
// the end of the line.
Help()
}
A literal is a fixed value assigned to variables or constants.
package main
import "fmt"
func main() {
// Integer literals
var i uint64
i = 42
i = 4_2
i = 0600
i = 0_600
i = 0o600
i = 0O600 // second character is capital letter 'O'
i = 0xBadFace
i = 0xBad_Face
i = 0x_67_7a_2f_cc_40_c6
i = 17014118346046923173
i = 170_141183_460469_23173
fmt.Println(i)
// Floating-point literals
var f float64
f = 0.
f = 72.40
f = 072.40 // == 72.40
f = 2.71828
f = 1.e+0
f = 6.67428e-11
f = 1e6
f = .25
f = .12345e+5
f = 1_5. // == 15.0
f = 0.15e+0_2 // == 15.0
f = 0x1p-2 // == 0.25
f = 0x2.p10 // == 2048.0
f = 0x1.Fp+0 // == 1.9375
f = 0x.8p-0 // == 0.5
f = 0x15e - 2 // == 0x15e - 2 (integer subtraction)
f = 0x_1FFFp-16 // == 0.1249847412109375
fmt.Println(f)
// Imaginary literals
var c complex128
c = 0i
c = 123i // == 123i for backward-compatibility
c = 0o123i // == 0o123 * 1i == 83i
c = 0xabci // == 0xabc * 1i == 2748i
c = 0.i
c = 2.71828i
c = 1.e+0i
c = 6.67428e-11i
c = 1e6i
c = .25i
c = .12345e+5i
c = 0x1p-2i // == 0x1p-2 * 1i == 0.25i
fmt.Println(c)
// Rune literals
var r rune
r = 'a'
r = '本'
r = '\'' // rune literal containing single quote character
r = '\t'
r = '\000'
r = '\007'
r = '\377'
r = '\x07'
r = '\xff'
r = '\u12e4'
r = '\U00101234'
r = 'ä'
fmt.Println(r)
// String literals
var s string
s = `abc` // same as "abc"
s = `\n
\n` // same as "\\n\n\\n"
s = "\n"
s = "\"" // same as `"`
s = "Hello, world!\n"
s = "日本語"
s = "\u65e5本\U00008a9e"
s = "\xff\u00FF"
s = "日本語" // UTF-8 input text
s = `日本語` // UTF-8 input text as raw literal
s = "\u65e5\u672c\u8a9e" // the explicit Unicode code points
s = "\U000065e5\U0000672c\U00008a9e" // the explicit Unicode code points
s = "\xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e" // the explicit UTF-8 bytes
fmt.Println(s)
}
When a package is imported, only entities (functions, types, variables, constants) whose names start with a capital letter can be used / accessed. The recommended style of naming in Go is that identifiers will be named using camelCase, except for those meant to be accessible across packages which should be PascalCase.
Module Name: Name your project in lowercase, otherwise it would look really weird in the imports.
Example: golang.org/x/tools
Package Name: For package, use singular noun instead of plural. Use user instead of users, notification instead of notifications.
File Name: For file name, try to keep it short, but still meaningful. Use lower case and use snake_case instead of mixedCaps. So, use server.go, notification.go or notification_server.go instead of notificationServer.go.
Function/Method: Use mixedCaps for naming your function. Use getArticles or GetArticles instead of get_articles or get-articles.
Variable: Name of variable should be short but should be meaningful enough. So, you can use srv or svc instead of service, server or myService. User or repo instead of repository or myRepository.
Constant: Constant follows the same rule with variables and functions, hence use mixedCaps for naming. No underscore unless it's generated code. So, use StatusActive instead of STATUS_ACTIVE.
Error: Name of error variable should start with Err. Use ErrNotFound instead of NotFoundError. Note that it's Err, not Error.
Interfaces: The simplest rule for naming interface is adding '-er' into the name of the action/method it represents.
See: https://pthethanh.herokuapp.com/blog/articles/golang-name-conventions
package main
import "fmt"
const myName = "Gábor"
func sayHello(name string) string {
return fmt.Sprintf("Hello, %v!", name)
}
// PrintHello prints a greeting for name.
func PrintHello(name string) {
fmt.Println(sayHello(name))
}
func main() {
PrintHello(myName)
}
Operators combine operands into expressions.
Expression = UnaryExpr | Expression binary_op Expression .
UnaryExpr = PrimaryExpr | unary_op UnaryExpr .
binary_op = "||" | "&&" | rel_op | add_op | mul_op .
rel_op = "==" | "!=" | "<" | "<=" | ">" | ">=" .
add_op = "+" | "-" | "|" | "^" .
mul_op = "*" | "/" | "%" | "<<" | ">>" | "&" | "&^" .
unary_op = "+" | "-" | "!" | "^" | "*" | "&" | "<-" .
Operator | Description |
---|---|
= |
normal assignment |
:= |
short assignment (short variable declaration, inside only functions) |
+= |
addition assignment |
-= |
subtraction assignment |
*= |
multiplication assignment |
/= |
division assignment |
%= |
remainder assignment |
&= |
bitwise AND assignment |
|= |
bitwise OR assignment |
^= |
bitwise XOR assignment |
<<= |
left shift assignment |
>>= |
right shift assignment |
&^= |
bit clear (AND NOT) assignment |
++ |
increase by one |
-- |
decrease by one |
Operator | Description |
---|---|
+ |
addition |
- |
subtraction |
* |
multiplication |
/ |
division |
% |
remainder |
& |
bitwise AND |
| |
bitwise OR |
^ |
bitwise XOR |
<< |
left shift |
>> |
right shift |
&^ |
bit clear (AND NOT) |
Operator | Description |
---|---|
== |
equal |
!= |
not equal |
< |
less than |
> |
greater than |
<= |
less than or equal |
>= |
greater than or equal |
Operator | Description |
---|---|
&& |
logical and |
|| |
logical or |
! |
logical not |
Operator | Description |
---|---|
... |
(ellipsis) pack/unpack parameters, arguments; implicit length |
, |
(comma) identifier list separator |
. |
(dot) import; struct-field, struct-method, qualified identifier separator |
; |
(semicolon) statement separator |
: |
(colon) slice maker; label end |
( |
parameter/argument list separator |
) |
parameter/argument list separator |
[ |
array, slice declaration, indexing; generic type |
] |
array, slice declaration, indexing; generic type |
{ |
block separator |
} |
block separator |
Operator | Description |
---|---|
& |
address of / create pointer |
* |
dereference pointer |
Operator | Description |
---|---|
<- |
send / receive operator |
Unary operators have the highest precedence. As the ++ and -- operators form statements, not expressions, they fall outside the operator hierarchy. As a consequence, statement *p++ is the same as (*p)++.
There are five precedence levels for binary operators. Multiplication operators bind strongest, followed by addition operators, comparison operators, && (logical AND), and finally || (logical OR):
Precedence Operator
5 * / % << >> & &^
4 + - | ^
3 == != < <= > >=
2 &&
1 ||
The blank identifier is represented by the underscore character _
. It serves
as an anonymous placeholder instead of a regular (non-blank) identifier.
KEYWORDS: const.
There are boolean constants, rune constants, integer constants, floating-point constants, complex constants, and string constants. Rune, integer, floating-point, and complex constants are collectively called numeric constants.
The predeclared constants are: true
, false
, iota
.
The special constant iota
:
- A counter which starts with zero.
- Increases by 1 after each line.
- Is only used with constant.
Iota keyword can be used on each line, or can be skipped as well.
package main
import "fmt"
// [const] Constants
// Pi exported constant, one value per line.
const Pi = 3.14 // const cannot be declared using the := syntax
// More constants by one const.
const (
truth = false // boolean constant
lf = '\n' // rune constant
statusOK = 200 // integer constant
pi = 3.1415965359 // floating-point constant
comp = 1.e+0i // complex constant
greeting = "Hello, world!" // string constant
)
const (
a = 0
b = 1
c = 2
)
// The same with auto increment IOTA.
const (
d = iota // 0
e // 1
f // 2
)
// Iota can also start from non-zero number - iota expressions can also be used
// to start iota from any number
const (
g = iota + 10 // 10
h // 11
i // 12
)
// Size is our type for 8 bit length unsigned integers.
type Size uint8
// Enum in Golang: IOTA provides an automated way to create a enum in Golang.
const (
small Size = iota
medium
large
extraLarge
)
func main() {
fmt.Println("truth:", truth)
fmt.Println("lf:", lf)
fmt.Println("statusOK:", statusOK)
fmt.Println("pi:", pi)
fmt.Println("comp:", comp)
fmt.Println("greeting:", greeting)
fmt.Println(small)
fmt.Println(medium)
fmt.Println(large)
fmt.Println(extraLarge)
}
KEYWORDS: var.
A variable is a storage location for holding a value. The set of permissible values is determined by the variable's type.
The predeclared zero value: nil
.
When storage is allocated for a variable, and no explicit initialization is
provided, the variable or value is given a default value. Each element of such
a variable or value is set to the zero value for its type: false
for
bool
eans, 0
for numeric types, ""
for string
s, and nil
for
pointers, func
tions, interface
s, slices, chan
nels, and map
s. This
initialization is done recursively.
package main
import "fmt"
// [var] Variables
// Declare one by one.
var male bool
// Name is exported if it begins with a Capital Letter.
var Name string
// Declare list of variables.
var (
age int
name string
location string
)
// [=] Declare and initialize.
var (
age2 int = 32
name2 string = "Interpreted Literal"
location2 string = `Raw String Literal`
)
// [=] Declare and initialize inferred.
var (
age3 = 42
name3 = "Initialized Literal"
location3 = "File"
)
// <ARRAY>
var myArr [10]int
var sentence [2]string
var multiDimension [2][3]string
// <SLICE>
var mySlice []int // <- nil slice, mySlice == nil: true
func main() {
// [:=] Inside a function, the := SHORT VARIABLE DECLARATION or
// short assignment statement.
name, location := "Its Me", "Here"
fmt.Println("name:", name, "location:", location)
raw := `Raw string\tliterals\n`
fmt.Println("raw string: ", raw)
hereDoc := `Here docs
or here strings can be
written easily by the
raw string literal.
`
fmt.Print("here doc: ", hereDoc)
interp := "Interpreted string\tliterals\n"
fmt.Print("interpreted string: ", interp)
lenRaw := len(raw)
fmt.Printf("Length of raw string: %v\n", lenRaw)
firstByte := raw[0] // strings can be handled as byte arrays
fmt.Println("First byte of raw string:", firstByte)
age := 32
fmt.Println("Age:", age)
// <ARRAY> initialization
sentence[0] = "Hello"
sentence[1] = "World"
myArr = [10]int{75, 42, 53, 67} // <ARRAY> initialization by literal
lenMyArr := len(myArr) // 10
fmt.Printf("Length of my array: %v\n", lenMyArr)
sen := [2]string{"hello", "world!"}
fmt.Println("String array of sentence, Println:", sen)
// [hello world!]
fmt.Printf("String array of sentence, Printf, %v: %s\n", "%s", sen)
// [hello world!]
fmt.Printf("String array of sentence, Printf, %v: %q\n", "%q", sen)
// ["hello" "world!"]
a := [...]string{"hello", "world!"} // [...] ellipsis: implicit length
fmt.Printf("String array with ellipsis, Printf, %v: %q\n", "%q", a)
multiDimension[0][1] = "42" // reference first row and second column
// <SLICE> initialization
mySlice = make([]int, 50, 100)
mySlice = new([100]int)[0:50] // equivalent
mySlice = []int{2, 3, 5, 7, 11, 13} // or initialization by literal
lenMySlice := len(mySlice) // 6
fmt.Printf("Length of my slice: %v\n", lenMySlice)
fmt.Println("mySlice: ", mySlice)
// [:] The slice maker operator: array[lowerIndex:higherIndex]
mySlice = myArr[1:3] // [:] s[lo:hi]: slice of elements lo - hi-1
mySlice = append(mySlice, 3) // add the number 3 to the right side
// Slices are like pointers to named or anonym arrays.
cities := make([]string, 3)
cities[0] = "Santa Monica"
cities[1] = "Venice"
cities[2] = "Los Angeles"
// Slices make array manipulation dynamic
cities = append(cities, "San Diego") // add to the right side
cities = append(cities, "San Diego", "Mountain View")
fmt.Printf("cities: %q\n", cities)
}
KEYWORDS: type.
A type determines a set of values together with operations and methods specific to those values. A type may be denoted by a type name, if it has one, or specified using a type literal, which composes a type from existing types.
Basic types: boolean, int, float, complex, rune, string.
Composite types: array, struct, pointer, function, interface, slice, map,
channel types.
Static type (or just type) of a variable is the type given in its
declaration.
Dynamic type, which is the concrete type of the value assigned to the
variable at run time.
The predeclared types:
- bool,
- byte, rune,
- float32, float64,
- int, int8, int16, int32, int64,
- uint, uint8, uint16, uint32, uint64, uintptr,
- complex64, complex128,
- string,
- error.
The documentation of builtin types: https://pkg.go.dev/builtin.
uint8 the set of all unsigned 8-bit integers (0 to 255)
uint16 the set of all unsigned 16-bit integers (0 to 65535)
uint32 the set of all unsigned 32-bit integers (0 to 4294967295)
uint64 the set of all unsigned 64-bit integers (0 to 18446744073709551615)
int8 the set of all signed 8-bit integers (-128 to 127)
int16 the set of all signed 16-bit integers (-32768 to 32767)
int32 the set of all signed 32-bit integers (-2147483648 to 2147483647)
int64 the set of all signed 64-bit integers (-9223372036854775808 to
9223372036854775807)
float32 the set of all IEEE-754 32-bit floating-point numbers
float64 the set of all IEEE-754 64-bit floating-point numbers
complex64 the set of all complex numbers with float32 real and imaginary parts complex128 the set of all complex numbers with float64 real and imaginary parts
byte alias for uint8 rune alias for int32
uint either 32 or 64 bits
int same size as uint
uintptr an unsigned integer large enough to store the uninterpreted bits of a
pointer value
The expression T(v)
converts the value v
to the type T
.
Some numeric conversions:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
Or, with short variable declaration:
i := 42
f := float64(i)
u := uint(f)
The assignment between items of different type requires an explicit conversion.
Package strconv implements conversions to and from string representations of basic data types.
i, err := strconv.Atoi("-42")
s := strconv.Itoa(-42)
b, err := strconv.ParseBool("true")
f, err := strconv.ParseFloat("3.1415", 64)
i, err := strconv.ParseInt("-42", 10, 64)
u, err := strconv.ParseUint("42", 10, 64)
s := strconv.FormatBool(true)
s := strconv.FormatFloat(3.1415, 'E', -1, 64)
s := strconv.FormatInt(-42, 16)
s := strconv.FormatUint(42, 16)
q := strconv.Quote("Hello, 世界")
q := strconv.QuoteToASCII("Hello, 世界")
A type assertion provides access to an interface value's underlying concrete value.
t := i.(T)
This statement asserts that the interface value i
holds the concrete type T
and assigns the underlying T
value to the variable t
.
If i
does not hold a T
, the statement will trigger a panic.
To test whether an interface value holds a specific type, a type assertion can return two values: the underlying value and a boolean value that reports whether the assertion succeeded.
t, ok := i.(T)
If i
holds a T
, then t
will be the underlying value and ok will be true.
If not, ok will be false
and t
will be the zero value of type T
, and no
panic occurs.
Note the similarity between this syntax and that of reading from a map.
m := make(map[string]int) // map
m["Answer"] = 42 // map has one entry
v, ok := m["Answer"] // reading from map similar
package main
import (
"fmt"
"strconv"
)
// [type]
type myFloat float64
type text []string
var (
debug bool = true
runCount int
version float32 = 1.0
unreal complex64 = 1.e+1i
lf rune = '\n'
name string = "Gábor"
)
func main() {
var pi myFloat = 3.14
var greeting text = text{"Welcome", "to", "the", "Go"}
fmt.Printf("pi: %v\n", pi)
fmt.Printf("greeting: %q\n", greeting)
// type casting
a := 42
f := float64(a)
u := uint(f)
fmt.Println(a, f, u)
// type conversion
i, err := strconv.Atoi("-42")
fmt.Println(i, err)
s := strconv.Itoa(-42)
fmt.Println(s)
b, err := strconv.ParseBool("true")
fmt.Println(b, err)
f, err = strconv.ParseFloat("3.1415", 64)
fmt.Println(f, err)
j, err := strconv.ParseInt("-42", 10, 64)
fmt.Println(j, err)
ui, err := strconv.ParseUint("42", 10, 64)
fmt.Println(ui, err)
s = strconv.FormatBool(true)
fmt.Println(s)
s = strconv.FormatFloat(3.1415, 'E', -1, 64)
fmt.Println(s)
s = strconv.FormatInt(-42, 16)
fmt.Println(s)
s = strconv.FormatUint(42, 16)
fmt.Println(s)
// type assertion
var ifc interface{} = "hello" // static type: interface, dynamic type: string
s = ifc.(string)
fmt.Println(s)
s, ok := ifc.(string)
fmt.Println(s, ok)
f, ok = ifc.(float64)
fmt.Println(f, ok)
//f = ifc.(float64) // panic!
//fmt.Println(f)
}
KEYWORDS: struct.
A struct is a sequence of named elements, called fields, each of which has a name and a type.
package main
import "fmt"
// [struct]
var aPoint struct {
x int
y int
}
type point struct {
x int
y int
}
// User struct is like an object of attributes.
type User struct {
ID int
Age int
Name, FirstName, LastName, Location string
}
// Player struct includes User and additional attribute
type Player struct {
User
GameID int
}
func main() {
// [struct] initialization
p := point{1, 2}
q := &point{1, 2} // has type *point
r := point{x: 1} // y:0 is implicit
s := point{} // x:0 and y:0
fmt.Printf("point{1, 2}: %v,\n", p)
fmt.Printf("&point{1, 2}: %v,\n", q)
fmt.Printf("point{x: 1}: %v,\n", r)
fmt.Printf("point{}: %v\n", s)
me := Player{}
me.ID = 47
me.Name = "Gábor"
me.Location = "Hungary"
fmt.Println(me)
}
package main
import "fmt"
// --- Anonymous Field in Struct ---
type Polygon struct {
Sides int
}
func (p *Polygon) NSides() int {
return p.Sides
}
type Triangle struct {
Polygon // Anonymous Field
}
// ---------------------------------
func main() {
pizza := struct {
name string
}{
name: "Pizza",
}
fmt.Println(pizza) // prints {Pizza}
// Using Anonymous Field of Struct to reduce unnecessary function calls.
t := Triangle{
Polygon{
Sides: 3,
},
}
fmt.Println(t.NSides()) // 3
}
KEYWORDS: map.
A map is an unordered group of elements of one type, called the element type, indexed by a set of unique keys of another type, called the key type.
package main
import "fmt"
//var env map[string]string
func main() {
// [map]
//env = make(map[string]string)
//env["PATH"] = "/bin"
scientists := map[string]int{
"Nikola Tesla": 166,
"Thomas Edison": 175,
"Tivadar Puskás": 178,
"James Clerk Maxwell": 191,
}
fmt.Printf("%#v\n", scientists)
env := map[string]string{
"PATH": "/bin:/usr/bin",
"SHELL": "/bin/bash",
"USER": "nobody",
}
key := "USER"
elem := "gabor"
// Insert or update an element in map m:
env[key] = elem
// Retrieve an element:
elem = env[key]
// Delete an element:
key = "SHELL"
delete(env, key)
// Test that a key is present with a two-value assignment:
elem1, ok := env[key]
// If key is in m, ok is true. If not, ok is false and elem is the zero value
// for the map’s element type.
if ok == false {
fmt.Printf("map: No key %v found in env:\n", key)
fmt.Printf("%q\n", env)
} else {
fmt.Println(elem1)
}
fmt.Println()
}
KEYWORDS: func.
A function type denotes the set of all functions with the same parameter and result types.
The predeclared functions: append, cap, close, complex, copy, delete, imag, len, make, new, panic, print, println, real, recover.
The documentation of builtin functions: https://pkg.go.dev/builtin.
append(slice []Type, elems ...Type) []Type
- The append built-in function appends elements to the end of a slice.cap(v Type) int
- The cap built-in function returns the capacity of v, according to its typeclose(c chan<- Type)
- The close built-in function closes a channel, which must be either bidirectional or send-only.complex(r, i FloatType) ComplexType
- The complex built-in function constructs a complex value from two floating-point values.copy(dst, src []Type) int
- The copy built-in function copies elements from a source slice into a destination slice.delete(m map[Type]Type1, key Type)
- The delete built-in function deletes the element with the specified key (m[key]) from the map.imag(c ComplexType) FloatType
- The imag built-in function returns the imaginary part of the complex number c.len(v Type) int
- The len built-in function returns the length of v, according to its type. In the most cases, this means the NUMBER OF ELEMENTS, except of STRINGs, where it is the NUMBER OF BYTES.make(t Type, size ...IntegerType) Type
- The make built-in function allocates and initializes an object of type slice, map, or chan (only).new(Type) *Type
- The new built-in function allocates memory.panic(v interface{})
- The panic built-in function stops normal execution of the current goroutine.print(args ...Type)
- The print built-in function formats its arguments in an implementation-specific way and writes the result to standard error.println(args ...Type)
- The println built-in function formats its arguments in an implementation-specific way and writes the result to standard error.real(c ComplexType) FloatType
- The real built-in function returns the real part of the complex number c.recover() interface{}
- The recover built-in function allows a program to manage behavior of a panicking goroutine.
package main
import "fmt"
// [func] function definition
// Can have more parameters.
func add(x int, y int) int {
// [return]
return x + y // [+]
}
// More parameters from the same type can be listed with same type.
func myMath(x, y float64) {
fmt.Println("x + y:", x+y) // [+] sum
fmt.Println("x - y:", x-y) // [-] difference
fmt.Println("x * y:", x*y) // [*] product
fmt.Println("x / y:", x/y) // [/] quotient
fmt.Println("x % y:", int(x)%int(y)) // [%] remainder
/*
& bitwise AND // [&]
| bitwise OR // [|]
^ bitwise XOR // [^]
&^ bit clear (AND NOT) // [&^]
*/
}
// Can return more values.
func sort(x, y int) (int, int) {
if x < y {
return x, y // [,]
}
return y, x
}
/*
Multiple return values can be named and act just like variables. If the
result parameters are named, a return statement without arguments returns the
current values of the results.
*/
func sort2(x, y int) (first, second int) {
if x < y {
first, second = x, y // [,]
} else {
first, second = y, x
}
return
}
// Variadic functions have variadic parameters.
// [...] ...Type: pack operator
func variadicFunc(arg ...int) {
for i := range arg {
fmt.Println("Variadic func", i, arg[i])
}
}
// <POINTER>
func inc(num *int) {
/*
By default Go passes arguments by value (copying the arguments), if you
want to pass the arguments by reference, you need to pass pointers (or use
a structure using reference values like slices and maps.
*/
// Dereference a pointer, use the [*] symbol.
*num++ // [++]
}
func dec(num *int) {
*num-- // [--]
}
// <METHOD>
// user struct is like an OBJECT of attributes.
type user struct {
id int
age int
name, firstName, lastName, location string
}
// NewUser as user's constructor-like function.
func NewUser(id, age int, firstName, lastName, location string) *user {
u := new(user)
u.id = id
u.age = age
u.firstName = firstName
u.lastName = lastName
u.location = location
return u
}
/*
Stringer [interface] is defined by the fmt package.
This is one of the most ubiquitous interfaces.
A STRINGER is a type that can describe itself as a string.
*/
// Stringer interface defines String() method.
type Stringer interface {
String() string
}
func (u *user) String() string {
return fmt.Sprintf("--- USER ---\nname:\t\t%v\nid:\t\t%v\nage:\t\t%v\nlocation:\t%v\n------------", u.Name(), u.id, u.age, u.location)
}
// Namer [interface] contains one method definition.
// An interface type is defined by a set of methods.
type Namer interface {
Name() string
}
// Name <METHOD> prints the user's name.
// Interfaces are satisfied implicitly.
func (u *user) Name() string {
return fmt.Sprintf("%s %s", u.firstName, u.lastName)
}
// Greetings <METHOD> is just a function with a RECEIVER ARGUMENT.
func (u *user) Greetings() string { // (u *user): method receiver
return fmt.Sprintf("Hi %s from %s", u.Name(), u.location)
}
// Birth returns by the birth year.
func (u *user) Birth() int {
return 2022 - u.age // [-]
}
// <GENERIC>
// Generics introduced in Go v1.18.
func printAnySlice[T any](s []T) {
for _, v := range s {
fmt.Print(v, " ")
}
fmt.Println("")
}
func main() {
// [func] call
fmt.Println("add(42, 13): ", add(42, 13))
fmt.Println("mymath(12.0, 33.0): ")
myMath(12.0, 33.0)
a, b := sort(42, 13)
fmt.Println("sort(42, 13): ", a, b)
a, b = sort2(42, 13)
fmt.Println("sort2(42, 13): ", a, b)
fmt.Println("variadicfunc(1, 2, 3):")
variadicFunc(1, 2, 3)
var nums []int = []int{42, 57, 93}
fmt.Println("variadicFunc(42, 57, 93):")
variadicFunc(nums...) // [...] Var...: unpack operator
age := 33
fmt.Println("age: ", age)
fmt.Println("inc(&age)")
inc(&age) // get the <POINTER> of a value, use the & symbol
fmt.Println("age: ", age)
fmt.Println("dec(&age)")
dec(&age) // get the <POINTER> of a value, use the & symbol
fmt.Println("age: ", age)
// Methods
/*
Instead of:
me := &user{} //me := new(user)
me.id = 1
me.age = 127
me.firstName = "Gábor"
me.lastName = "Imolai"
me.location = "Hungary"
*/
// We call "constructor".
me := NewUser(1, 127, "Gábor", "Imolai", "Hungary")
fmt.Println(me) // Print via Stringer.
printAnySlice(nums)
}
package main
import "fmt"
func main() {
// Anonymous function
v := func() {
fmt.Println("Inside anonymous function")
}
v() // call
func() {
fmt.Println("Anonymous function gets invoked immediately")
}()
func(v int) {
fmt.Println(v)
}(42) // Passing Arguments to an Anonymous Function
g := func(v string) {
fmt.Println(v)
}
func(v string, g func(v string)) {
g(v)
}("Passing Anonymous Functions as an Argument", g)
f1 := func() func(v string) {
f := func(v string) {
fmt.Println(v)
}
return f
}
h := f1()
h("Returning Anonymous Function from a Function")
}
KEYWORDS: goto.
A "goto" statement transfers control to the statement with the corresponding label within the same function.
package main
import "fmt"
// [goto]
func funGoTo() {
fmt.Println("Before a goto.")
goto FINISH
fmt.Println("Unreachable code due to goto.")
FINISH: // label
fmt.Println("After a goto.")
}
func main() {
funGoTo()
}
KEYWORDS: return.
A "return" statement in a function F terminates the execution of F, and optionally provides one or more result values.
package main
import "fmt"
func add(x int, y int) int {
// [return]
return x + y // [+]
}
// Can return more values.
func sort(x, y int) (int, int) {
if x < y {
return x, y // [,]
}
return y, x
}
/*
Multiple return values can be named and act just like variables. If the
result parameters are named, a return statement without arguments returns the
current values of the results.
*/
func sort2(x, y int) (first, second int) {
if x < y {
first, second = x, y // [,]
} else {
first, second = y, x
}
return
}
func main() {
fmt.Println("add(42, 13): ", add(42, 13))
fmt.Println("mymath(12.0, 33.0): ")
a, b := sort(42, 13)
fmt.Println("sort(42, 13): ", a, b)
a, b = sort2(42, 13)
fmt.Println("sort2(42, 13): ", a, b)
}
KEYWORDS: defer.
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.
package main
import (
"fmt"
"os"
)
// [defer]
/*
Defer statement is used to execute a function call just before the
surrounding function where the defer statement is present returns
When a function has multiple defer calls, they are pushed on to a stack an
executed in Last In First Out (LIFO) order.
*/
func writeToTempFile(text string) error {
file, err := os.Open("temp.txt")
if err != nil {
return err
}
defer file.Close()
n, err := file.WriteString(text)
if err != nil {
return err
}
fmt.Printf("Number of bytes written: %d", n)
return nil
}
func main() {
writeToTempFile("Some text from deferred function.")
}
KEYWORDS: interface.
An interface type specifies a method set called its interface. A variable of interface type can store a value of any type with a method set that is any superset of the interface. Such a type is said to implement the interface.
package main
import (
"fmt"
"time"
)
// user struct is like an OBJECT of attributes.
type user struct {
id int
age int
name, firstName, lastName, location string
}
// [interface]
// Namer [interface] contains one method definition.
// An interface type is defined by a set of methods.
type Namer interface {
Name() string
}
// Name <METHOD> prints the user's name.
// Interfaces are satisfied implicitly.
func (u *user) Name() string {
return fmt.Sprintf("%s %s", u.firstName, u.lastName)
}
/*
Stringer [interface] is defined by the fmt package.
This is one of the most ubiquitous interfaces.
A STRINGER is a type that can describe itself as a string.
*/
// Stringer interface defines String() method.
type Stringer interface {
String() string
}
func (u *user) String() string {
return fmt.Sprintf("--- USER ---\nname:\t\t%v\nid:\t\t%v\nage:\t\t%v\nlocation:\t%v\n------------", u.Name(), u.id, u.age, u.location)
}
// The ERROR type is a built-in interface similar to fmt.Stringer:
type error interface {
Error() string
}
type MyError struct {
When time.Time
What string
}
func (e *MyError) Error() string {
return fmt.Sprintf("at %v, %s",
e.When, e.What)
}
func run() error {
return &MyError{
time.Now(),
"it didn't work",
}
}
func main() {
me := new(user)
me.id = 1
me.age = 127
me.firstName = "Gábor"
me.lastName = "Imolai"
me.location = "Hungary"
fmt.Println(me) // Print via Stringer.
}
KEYWORDS: if.
"If" statements specify the conditional execution of two branches according to the value of a boolean expression.
package main
import "fmt"
func main() {
// [if]
answer := 42
if answer == 42 { // [==]
fmt.Println("The Answer to the Ultimate Question of Life, the Universe, and Everything...")
}
foo := func() interface{} {
return nil
}
// Like for, the if statement can start with a short statement.
if err := foo(); err != nil { // [!=]
panic(err)
}
// Nested ifs can be avoided by chaining conditions logically.
question := "What do you get if you multiply six by nine?"
if question == "What do you get if you multiply six by nine?" && answer == 42 {
fmt.Println("Don't panic!")
}
}
KEYWORDS: else.
If the expression evaluates to true, the "if" branch is executed, otherwise, if present, the "else" branch is executed.
package main
import "fmt"
func main() {
// [if] [else]
magicNum := 100
if magicNum == 100 {
fmt.Println("Japan")
} else {
fmt.Println("Canada")
}
// [if] [else] [if]
if magicNum == 50 {
fmt.Println("Germany")
} else if magicNum == 100 {
fmt.Println("Japan")
} else {
fmt.Println("Canada")
}
}
KEYWORDS: switch.
"Switch" statements provide multi-way execution. An expression or type is compared to the "cases" inside the "switch" to determine which branch to execute.
In an expression switch, the cases contain expressions that are compared against the value of the switch expression.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// We can write multiple if statements.
age := 42
if age == 16 {
fmt.Println("Too young")
}
if age == 42 {
fmt.Println("Adult")
}
if age == 70 {
fmt.Println("Senior")
}
// But switch is more readable.
// [switch] [case]
switch age {
case 16:
fmt.Println("Too young")
case 42:
fmt.Println("Adult")
case 70:
fmt.Println("Senior")
}
// Switch with a short statement.
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Linux.")
}
t := time.Now()
// Switch without a condition is the same as switch true.
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
}
}
In a type switch, the cases contain types that are compared against the type of a specially annotated switch expression.
A type switch is a construct that permits several type assertions in series.
A type switch is like a regular switch statement, but the cases in a type switch specify types (not values), and those values are compared against the type of the value held by the given interface value.
switch v := i.(type) {
case T:
// here v has type T
case S:
// here v has type S
default:
// no match; here v has the same type as i
}
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case bool:
fmt.Printf("Boolean: %v\n", v)
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
KEYWORDS: case.
In an expression switch, the switch expression is evaluated and the case expressions, which need not be constants, are evaluated left-to-right and top-to-bottom; the first one that equals the switch expression triggers execution of the statements of the associated case; the other cases are skipped.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// [switch] [case]
score := 7
switch score {
case 0, 1, 3:
fmt.Println("Terrible")
case 4, 5:
fmt.Println("Mediocre")
case 6, 7:
fmt.Println("Not bad")
case 8, 9:
fmt.Println("Almost perfect")
case 10:
fmt.Println("hmm did you cheat?")
}
// Switch with a short statement.
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
}
t := time.Now()
// Switch without a condition is the same as switch true.
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
}
}
KEYWORDS: default.
If no case matches and there is a "default" case, its statements are executed. There can be at most one default case and it may appear anywhere in the "switch" statement.
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
// [switch] [case]
score := 7
switch score {
case 0, 1, 3:
fmt.Println("Terrible")
case 4, 5:
fmt.Println("Mediocre")
case 6, 7:
fmt.Println("Not bad")
case 8, 9:
fmt.Println("Almost perfect")
case 10:
fmt.Println("hmm did you cheat?")
default:
fmt.Println(score, " off the chart")
}
// Switch with a short statement.
switch os := runtime.GOOS; os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.\n", os)
}
t := time.Now()
// Switch without a condition is the same as switch true.
switch {
case t.Hour() < 12:
fmt.Println("Good morning!")
case t.Hour() < 17:
fmt.Println("Good afternoon.")
default:
fmt.Println("Good evening.")
}
}
KEYWORDS: fallthrough.
In a case or default clause, the last non-empty statement may be a (possibly labeled) "fallthrough" statement to indicate that control should flow from the end of this clause to the first statement of the next clause. Otherwise control flows to the end of the "switch" statement. A "fallthrough" statement may appear as the last statement of all but the last clause of an expression switch.
package main
import "fmt"
func main() {
// [fallthrough]
n := 4
switch n {
case 0:
fmt.Println("is zero")
fallthrough
case 1:
fmt.Println("is <= 1")
fallthrough
case 2:
fmt.Println("is <= 2")
fallthrough
case 3:
fmt.Println("is <= 3")
fallthrough
case 4:
fmt.Println("is <= 4")
fallthrough
case 5:
fmt.Println("is <= 5")
fallthrough
case 6:
fmt.Println("is <= 6")
fallthrough
case 7:
fmt.Println("is <= 7")
fallthrough
case 8:
fmt.Println("is <= 8")
fallthrough
default:
fmt.Println("Try again!")
}
}
KEYWORDS: for.
A "for" statement specifies repeated execution of a block. There are three forms: The iteration may be controlled by a single condition, a "for" clause, or a "range" clause.
package main
func main() {
// [for]
sum := 0
// Traditional for loop
for i := 0; i < 10; i++ {
sum += i
}
sum = 1
// Loop without pre/post statements
for sum < 1000 {
sum += sum
}
sum = 1
// For loop as a while loop
for sum < 1000 {
sum += sum
}
// Infinite loop
//for {
// do something in a loop forever
//}
}
KEYWORDS: break.
A "break" statement terminates execution of the innermost "for", "switch", or "select" statement within the same function.
package main
import "fmt"
func main() {
// [break]
for i := 1; i <= 5; i++ {
// terminates the loop when i is equal to 3
if i == 3 {
break
}
fmt.Println(i)
}
}
KEYWORDS: continue.
A "continue" statement begins the next iteration of the innermost "for" loop at its post statement.
package main
import "fmt"
func main() {
// [continue]
for i := 1; i <= 5; i++ {
// skips the iteration when i is equal to 3
if i == 3 {
continue
}
fmt.Println(i)
}
}
KEYWORDS: range.
A "for" statement with a "range" clause iterates through all entries of an array, slice, string or map, or values received on a channel. For each entry it assigns iteration values to corresponding iteration variables if present and then executes the block.
package main
import "fmt"
func printList(args ...int) {
for i, arg := range args {
fmt.Println("Elem", i, arg)
}
}
func main() {
// [range]
printList(42, 57, 63, 71)
cities := []string{"Barcelona", "Budapest", "Belgrad", "Wien"}
fmt.Println("cities printed in range:")
for i, city := range cities {
// for _, city := range cities { // skip the index
fmt.Printf("%v) %v\n", i, city)
}
}
KEYWORDS: package.
Go applications are organized in packages. Package is a way of grouping related code. Every Go source file (*.go) in a Go application belongs to a package. Package can be of two types.
- Executable package – Only main is the executable package in Go. A .go file
might belong to the main package present within a specific directory. We
will see later how the directory name or the .go file name matters. The main
package will contain a main function that denotes the start of a program. On
installing the main package it will create an executable in the
$GOBIN
directory. - Utility package – Any package other than the main package is a utility package. It is not self-executable. It just contains the utility function and other utility things that can be utilized by an executable package.
$ ls ~/sdk/go1.17.6/src/math/*.go | xargs grep ^package | cut -d: -f2 | sort -u
package math
package math_test
Choose a good import path (make your package "go get"-able). Import paths should
be globally unique, so use the path of your source repository as its base. For
instance, the websocket package from the go.net sub-repository has an import
path of golang.org/x/net/websocket
. The Go project owns the path
github.com/golang
, so that path cannot be used by another author for a
different package.
Instead of public, private or protected keywords, to control the visibility is using the capitalized and non-capitalized formats.
- Capitalized Identifiers are exported. The capital letter indicates that this is an exported identifier. It will be visible in other packages.
- Non-capitalized identifiers are not exported. The lowercase indicates that the identifier is not exported and will only be accessed from within the same package.
Module is a way of dealing with dependencies. A module by definition is a
collection of related packages with go.mod
at its root. The go.mod file
defines the
- Module import path.
- Dependency requirements of the module for a successful build. It defines both project’s dependencies requirement and also locks them to their correct version
Consider a module as a directory containing a collection of packages. The packages can be nested as well. Modules provide
- Dependency Management
- With modules go project doesn’t necessarily have to lie in the
$GOPATH/src
folder.
-
Make package directory
$ mkdir greeting $ cd greeting
-
Initialize module
greeting$ go mod init example.com/greeting
-
Make package code(s)
greeting$ vi greeting.go
-
Run/build application
greeting$ go run greeting.go // AND/OR greeting$ go build greeting.go
package main
import (
"fmt"
"math/rand" // It is defined in the "rand" directory in the "math" directory
// as "rand.go" and "package rand" inside.
/*
~$ ls ~/sdk/go1.17.6/src/math/rand/rand.go
/home/$USER/sdk/go1.17.6/src/math/rand/rand.go
~$ grep ^package ~/sdk/go1.17.6/src/math/rand/rand.go
package rand
*/
"time"
)
func main() {
// In Go, a name is exported if it begins with a capital letter. Like the
// Seed, Now, Println, Intn.
rand.Seed(time.Now().UnixNano())
fmt.Println("Random number between 0 and 10: ", rand.Intn(10))
}
KEYWORDS: import.
An import declaration states that the source file containing the declaration depends on functionality of the imported package and enables access to exported identifiers of that package.
package main
// [import]
/*
Almost everything we want to do in Go comes from some package. And to use
those packages, we need to import them.
**Aliased import**
import f "fmt"
**Blank import**
import _ "time" ignore the unused import
**Dot import** [.]
If an explicit period (.) appears instead of a name, all the package's
exported identifiers will be declared in the current file's file block an
can be accessed without a qualifier
Import declaration Local name of Sin
import "lib/math" math.Sin
import M "lib/math" M.Sin
import . "lib/math" Sin
*/
//import "fmt" // import packages one by one
//import "os"
import ( // grouped import syntax
"fmt"
"os"
)
func main() {
fmt.Println()
os.Exit(0)
}
KEYWORDS: go.
A "go" statement starts the execution of a function call as an independent concurrent thread of control, or goroutine, within the same address space.
package main
import (
"fmt"
"time"
)
// [go]
func printNums(from, to int) {
for i := from; i <= to; i++ {
time.Sleep(50 * time.Millisecond)
fmt.Print(i, " ")
}
}
func main() {
fmt.Println("Print nums by Goroutines.")
go printNums(0, 5)
go printNums(6, 10)
// No output, because the main() does not wait, exits before the Goroutines.
// It is a simple solution to put a wait in the main thread:
time.Sleep(500 * time.Millisecond)
// Anonymous Goroutine
go func() {
for i := 11; i < 16; i++ {
fmt.Print(i, " ")
time.Sleep(100 * time.Millisecond)
}
}()
time.Sleep(1 * time.Second)
}
KEYWORDS: chan.
A channel provides a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type.
A new, initialized channel value can be made using the built-in function make
,
which takes the channel type and an optional capacity as arguments (size of
the buffer).
A channel may be closed with the built-in function close
.
A deadlock happens when a group of goroutines are waiting for each other and none of them is able to proceed. The program will get stuck on the channel send operation waiting forever for someone to read the value.
Deadlock occurs:
- Receiving Channels More Than Expected
- Sending Channels More Than Expected
The solution to resolve Channel Deadlock:
Number Of Sending Channels = Number Of Receiving Channels
If channel is an unbuffered channel, then sending will be blocked until it is ready to receive. It is easier if we use a buffered channel, which accepts a limited number of values without any blocking.
package main
import "fmt"
func printNumsCh(from, to int, ch chan bool) {
for i := from; i <= to; i++ {
fmt.Print(i, " ")
}
ch <- true // [<-]
}
func main() {
// [chan]
/*
To enable communication between Goroutines, Go provides Channels. A channel
is like a pipe, allowing you to send and receive data from Goroutines.
*/
fmt.Println("Print nums by Goroutines using Channels.")
ch := make(chan bool)
workers := 2
go printNumsCh(0, 5, ch)
go printNumsCh(6, 10, ch)
// [<-]
for i := workers; i > 0; i-- {
<-ch // or: value := <-ch
}
fmt.Println()
}
KEYWORDS: select.
A "select" statement chooses which of a set of possible send or receive operations will proceed. It looks similar to a "switch" statement but with the cases all referring to communication operations.
package main
import "fmt"
func sumEven(from, to int, ch chan int) {
sum := 0
for i := from; i <= to; i++ {
if i%2 == 0 {
sum += i
}
}
ch <- sum // [<-]
}
func sumOdd(from, to int, ch chan int) {
sum := 0
for i := from; i <= to; i++ {
if i%2 != 0 {
sum += i
}
}
ch <- sum // [<-]
}
func main() {
// [select]
/*
The select statement is used to wait on multiple channel operations.
The syntax is similar to switch except that each of the case statements
will be a channel operation.
*/
fmt.Println("Print even and odd sums by Goroutines using Channels.")
even := make(chan int)
odd := make(chan int)
workers := 2
go sumEven(0, 100, even)
go sumOdd(0, 100, odd)
for i := workers; i > 0; i-- {
select {
case x := <-even:
fmt.Println("Even:", x)
case y := <-odd:
fmt.Println("Odd:", y)
}
}
}
- How to Write Go Code
- A Tour of Go
- Effective Go
- The Go Programming Language Specification
- Go Bootcamp
- How To Code in Go
- An Introduction to Programming in Go
- Practical Go Lessons
- All About GoLang
- Golang Advanced Tutorial
- Programming.Guide/Go
- Go by Example
- Go Language Patterns
- Go on Exercism
- Oldbloggers Golang posts
Gábor Imolai
Contributions, issues, and feature requests are welcome!
Give a ⭐️ if you like this project!