Go is good.
- Knowledge of any other programming language is a plus but not a requirement.
In Go, code is organized into packages. A package is a collection of Go files in the same directory that are compiled together. Each Go file starts with a package statement that defines the package it belongs to.
There are two types of packages: the main package and other packages.
- Main Package:
This is the starting point of any Go application. A file with the
package main
statement defines the main package, and it must contain amain()
function. Themain()
function is the entry point of the program where the execution starts. - Other Packages (Library Package):
Other packages are used to organize code into reusable components. These packages group related functions, types, and variables together, making it easier to manage and maintain the code.
Important
In Go, only projects containing a main
package with a main
function can be executed directly as standalone programs. Other non-main packages, which do not include a main
function, cannot be run on their own. These packages are designed as libraries or helpers that can be imported and used by other packages, including those with a main
package.
A collection of related Go packages is called a module. A module has a go.mod
file at the root of the module directory that defines the module's path and dependencies.
Note
The go.mod
file is essential for both main packages and library packages. We create it every time we create a new project.
-
Initializing a module:
The
<module-path>
is the root import path for all packages within the module and is usually the URL of the repository where the module is hosted.go mod init <module-path>
So, running
go mod init github.com/username/project
will generate ago.mod
file like this:module github.com/username/project go 1.22.4
-
Create a directory called
myapp
for the project. -
Initialize a project (module):
go mod init github.com/username/myapp
-
Create a
main.go
file:package main import "fmt" func main() { fmt.Println("Hello, World!") }
-
We can create a different Go file (called
calculate.go
) under the same package:package main func add(a, b int) int { return a + b } func subtract(a, b int) int { return a - b }
-
Using the new functions in the main file:
You don't need to import the functions if they are defined in the same package. They are already part of the package.
package main import "fmt" func main() { fmt.Println(add(1, 2)) // 3 fmt.Println(subtract(5, 3)) // 2 }
Overall directory structure:
myapp/ ├── go.mod ├── main.go └── calculate.go
Note
A folder can only have files that belong to the same package.
Note
You cannot have multiple functions with the same name under a single package, even if they are in different files.
Overall directory structure:
myapp/ ├── go.mod ├── main.go └── calculate/ ├── basic.go └── advanced.goThe
go.mod
file remains in the root directory and handles the dependencies for the entire project. You don't need separatego.mod
files for nested packages.
-
main.go:
You need to import the nested packages because they are not part of the importing package (
main
in this case).When you import nested packages, you need to use their full paths relative to the module path.
github.com/username/myapp
->/calculate
=github.com/username/myapp/calculate
.package main import ( "fmt" "github.com/username/myapp/calculate" ) func main() { fmt.Println(calculate.Add(1, 2)) // 3 fmt.Println(calculate.Subtract(5, 3)) // 2 fmt.Println(calculate.Square(5)) // 25 fmt.Println(calculate.SquareRoot(25)) // 5 }
-
calculate/
-
basic.go:
package calculate func Add(a, b int) int { return a + b } func Subtract(a, b int) int { return a - b }
-
advanced.go:
package calculate import "math" func Square(x int) int { return x * x } func SquareRoot(x int) float64 { return math.Sqrt(float64(x)) }
-
Important
In Go, only identifiers (variables, functions, types, etc.) that start with an uppercase letter are exported and can be accessed from other packages.
Overall directory structure:
mylib/ ├── go.mod ├── mylib.go └── generate/ ├── name.go └── surname.go
-
Create a directory called
mylib
for the library project. -
Initializing a library module:
go mod init github.com/username/mylib
-
mylib.go:
package mylib import ( "fmt" "github.com/username/mylib/generate" ) func SayHi() { fmt.Printf("Hi %s!\n", generate.Name()) } func SayBye() { fmt.Printf("Bye %s!\n", generate.Surname()) } func GetFullName() string { return fmt.Sprintf("%s %s", generate.Name(), generate.Surname()) }
-
generate/:
-
name.go:
package generate func Name() string { return "John" }
-
surname.go:
package generate func Surname() string { return "Doe" }
-
Todo:
#4. Using an external library/package in a Go project.
-
Boolean:
Keyword Values bool
true
/false
Zero value (default):
false
-
Integer:
Keyword Size Values int
32 bits / 64 bits int32 / int64 int8
8 bits -128 to 127 int16
16 bits -32768 to 32767 int32
32 bits -2147483648 to 2147483647 int64
64 bits -9223372036854775808 to 9223372036854775807 uint
32 bits / 64 bits uint32 / uint64 uint8
(alias forbyte
)8 bits 0 to 255 uint16
16 bits 0 to 65535 uint32
32 bits 0 to 4294967295 uint64
64 bits 0 to 18446744073709551615 Zero value (default):
0
-
Float:
Keyword Size Values float32
32 bits -3.4e+38 to 3.4e+38 float64
64 bits -1.7e+308 to +1.7e+308 Zero value (default):
0
-
String:
Keyword Value string
"anything surrounded by double quotes" Zero value (default):
""
-
Using the
var
keyword:var varName type = value
- You always have to specify either
type
orvalue
(or both):var name string = "John" var surname = "Doe" // Go can infer the type of the variable from the initial value. var age int // zero value: 0
- Declaring multiple variables of the same or different types in a single line:
var name, age = "John", 30 var a, b, c int = 1, 2, 3 // If the type keyword is used, it is only possible to declare one type of variable per line.
- Using grouped declaration syntax to declare variables together:
var ( name string = "John" surname = "Doe" age int )
- You always have to specify either
-
Using the
:=
syntax:Then you use this syntax you must assign the value to the variable at the time of declaration.
varName := value
- This syntax is only available inside functions.
- Declaring multiple variables in a single line:
name, age := "John", 30
Note
Variables declared without an explicit initial value are given their zero value.
-
Constants:
Constants are fixed values that cannot be changed once they are set. They are read-only.
const varName type = value
- Constants can be declared without explicit types, but they must be declared with values:
const daysInWeek int = 7 const hoursInDay = 24
- Declaring multiple constants in a single block:
const ( daysInWeek = 7 hoursInDay = 24 )
- Computations on constants are done at compile time, not at runtime.
- Constants can be declared without explicit types, but they must be declared with values:
Placeholders | Description | Usage | Output |
---|---|---|---|
%v |
Default placeholder for everything. | ("%v", 123) |
123 |
%s |
Plain string. | ("%s", "hello") |
hello |
%q |
String with double quotes. | ("%q", "hello") |
"hello" |
%d |
Integer in base 10. | ("%d", 123) |
123 |
%b |
Integer in base 2. | ("%b", 123) |
1111011 |
%o |
Integer in base 8. | ("%o", 123) |
173 |
%x |
Integer in base 16 with lowercase letters. | ("%x", 123) |
7b |
%X |
Integer in base 16 with uppercase letters. | ("%X", 123) |
7B |
%c |
Integer in Unicode code point. | ("%c", 65) |
A |
%f |
Floating-point number in decimal format. | ("%f", 123.456) |
123.456000 |
%e |
Floating-point number in scientific notation with a lowercase 'e' | ("%e", 123.456) |
1.234560e+02 |
%E |
Floating-point number in scientific notation with an uppercase 'E'. | ("%E", 123.456) |
1.234560E+02 |
%t |
Boolean value. | ("%t", true) |
true |
%T |
Prints the type of the value. | ("%T", 123) |
int |
%+v |
Prints struct fields with their names. | ("%+v", Person{Name: "John", Age: 35}) |
{Name:John Age:35} |
%% |
Prints a literal % sign. |
("%%") |
% |
Examples:
- Printing a string and an integer:
func main() { name := "John" age := 35 fmt.Printf("Name: %s, Age: %d", name, age) // Name: John, Age: 35 }- Formatting a floating-point number:
func main() { pi := 3.14159 fmt.Printf("Pi: %.2f", pi) // Pi: 3.14 }
- Declaring a function:
func add(a int, b int) int { return a + b }
- Calling a function:
result := add(2, 3) // result = 5
Extra:
-
Functions can have multiple return values:
func swap(a, b int) (int, int) { return b, a }
x, y := 1, 2 x, y = swap(x, y) // x = 2, y = 1
-
Ignoring return values:
Ignoring return values is useful when you don't need to use the return values of a function.
x, _ = getCoords()
-
Named return values:
When you have named return values, you can use the return keyword without specifying the values. It will automatically return the named return values. However, using the return keyword without specifying the values can hurt readability, so it is not recommended.
func calculate(x int) (result int) { result = x * x return }
-
Variadic functions:
Variadic functions are functions that can accept a variable number of arguments.
func sum(numbers ...int) int { total := 0 for _, number := range numbers { total += number } return total }
-
First-class functions:
First-class functions are functions that can be assigned to variables, passed as arguments to other functions, and returned as values from other functions.
func main() { price := 100.00 fmt.Printf("Regular user price: $%.2f\n", applyDiscount(price, regularDiscount)) // Regular user price: $90.00 fmt.Printf("VIP user price: $%.2f\n", applyDiscount(price, vipDiscount)) // VIP user price: $80.00 } func vipDiscount(price float64) float64 { return price * 0.8 // discount 20% } func regularDiscount(price float64) float64 { return price * 0.9 // discount 10% } func applyDiscount(price float64, strategy func(float64) float64) float64 { return strategy(price) }
-
Anonymous functions:
Anonymous functions are functions that don't have a name.
func main() { func() { fmt.Println("Hello, world!") }() // <-- Immediately invoking the anonymous function }
- Assigning the anonymous function to a variable:
add := func(a, b int) { fmt.Println(a + b) } add(1, 2) // 3
- Assigning the anonymous function to a variable:
The defer
keyword allows us to execute a function or line of code at the end of the current function's execution. This is useful for cleaning up resources, such as closing files or connections.
func main() {
defer sayBye()
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
fmt.Println(myFunc())
}
func myFunc() string {
defer fmt.Println("4")
return "5"
}
func sayBye() {
fmt.Println("bye")
}
Output:
1 3 4 5 2 byeMultiple deferred statements are executed in last-in-first-out (LIFO) order. This means that the most recently deferred function is executed first.
Closures are functions that can access and manipulate variables from their outer scope, even after the outer function has finished executing.
func main() {
count1 := adder()
count2 := adder()
fmt.Println(count1(1)) // 1
fmt.Println(count1(2)) // 3
fmt.Println(count1(3)) // 6
fmt.Println(count2(2)) // 2
fmt.Println(count2(4)) // 6
fmt.Println(count2(6)) // 12
}
func adder() func(int) int {
sum := 0
return func(number int) int {
sum += number
return sum
}
}
-
Pass-by-Value
In Go, functions use
pass-by-value
for arguments. This means that when you pass a value to a function, Go creates a copy of that value and passes the copy to the function. The function works with this copy, so changes made to the parameter inside the function do not affect the original value.func main() { num := 5 increment(num) fmt.Println("Outside:", num) // Outside: 5 } func increment(num int) { num++ fmt.Println("Inside:", num) // Inside: 6 }
-
Pass-by-Reference with Pointers
If you need to modify the original value, you can use pointers. A pointer holds the memory address of a value. When you pass a pointer to a function, you're passing the address of the value, not a copy of it. This allows the function to modify the original value.
func main() { num := 5 increment(&num) fmt.Println("Outside:", num) // Outside: 6 } func increment(num *int) { *num++ fmt.Println("Inside:", *num) // Inside: 6 }
Pointers in Go allow you to directly reference the memory address of a variable. This allows you to directly access and modify the value stored in that memory location.
&
: To get the memory address of a variable.*
: To access or modify the value at the memory address.
func main() {
// Declaring a variable:
a := 10
// Declaring a pointer and assigning it the address of 'a':
ptr := &a
fmt.Println(a) // 10
fmt.Printf("%T\n", a) // int (type of 'a')
fmt.Println(ptr) // 0xc0000a4010 (memory address of 'a')
fmt.Printf("%T\n", ptr) // *int (type of 'ptr')
fmt.Println(*ptr) // 10 (value at the memory address)
// Changing the value at the memory address:
*ptr = 20
fmt.Println(a) // 20
}
Passing pointers to functions allows you to modify the original value:
func main() {
num := 10
fmt.Println(num) // 10
resetVal(num)
fmt.Println(num) // 10
resetPtr(&num)
fmt.Println(num) // 0
}
func resetVal(val int) {
val = 0
}
func resetPtr(val *int) {
*val = 0
}
Warning
When a pointer does not point to a valid memory address, it is called a nil
pointer. If you try to dereference a nil
pointer, you will get a panic: runtime error: invalid memory address or nil pointer dereference
error.
- Bad: (this code will panic)
func main() { // Declaring a pointer (zero valued): var ptr *int fmt.Println(ptr) // <nil> reset(ptr) } func reset(val *int) { *val = 0 // panic: runtime error: invalid memory address or nil pointer dereference }
- Good. Handling
nil
pointers: (this code will return an error instead of panicking)func main() { var ptr *int fmt.Println(ptr) // <nil> fmt.Printf("reset(ptr): %v\n", reset(ptr)) // reset(ptr): invalid pointer } func reset(val *int) error { if val == nil { return errors.New("invalid pointer") } *val = 0 return nil }
Tip
Using pointers in Go can greatly improve performance by reducing memory usage and increasing speed, as they allow large data structures to be passed to functions without being copied. This efficiency comes from directly referencing memory addresses, which avoids the overhead of duplicating data.
However, pointers add complexity and can lead to memory-related bugs if not used carefully.
Structs in Go are a way to group related variables under a single name.
- Defining a struct:
type person struct { name string age int }
- Creating a struct instance:
or zero valued:
user := person{name: "John", age: 35}
var user person
- Accessing and modifying struct fields:
fmt.Println(user.name) // John user.age = 30
Extra:
-
Embedded and nested structs:
type address struct { city string state string zipCode string } type contact struct { phone string email string } type person struct { name string age int address // Embedded struct contact contact // Nested struct } func main() { user := person{ name: "John", age: 35, address: address{ city: "Los Angeles", state: "California", zipCode: "00000", }, contact: contact{ phone: "(000) 000-0000", email: "[email protected]", }, } fmt.Println(user) // {John 35 {Los Angeles California 00000} {(000) 000-0000 [email protected]}} fmt.Println(user.city) // Los Angeles fmt.Println(user.contact.phone) // (000) 000-0000 }
- Defining a method:
func (p person) sayHello() { fmt.Println("Hello, my name is " + p.name) }
- Calling a method:
user := person{name: "John", age: 35} user.sayHello() // Hello, my name is John
Extra:
-
Manipulating using methods:
Here, we need to use pointers to manipulate the struct through the method because of the pass-by-value rule.
func main() { user := person{name: "John", age: 35} user.sayHello() // Hello, my name is John user.changeName("Sha'an") user.sayHello() // Hello, my name is Sha'an } func (p *person) changeName(name string) { p.name = name }
In Go, array is a fixed-size sequence of elements of the same type.
- Declaring an array:
var arr [5]int // zero valued
- Initializing at the time of declaration:
arr := [5]int{1, 2, 3, 4, 5}
- Initializing at the time of declaration:
- Accessing array elements:
fmt.Println(arr[0]) // 1
- Modifying array elements:
arr[0] = 100 fmt.Println(arr[0]) // 100
Extra:
- Letting the compiler decide the size of the array:
arr := [...]int{1, 5: 2, 3, 4, 5}
- Initializing specific indexes of an array:
When you use the index: value syntax, you specify the value for a particular index in the array. This allows you to skip some indices (with zero values) and directly assign values to others.
arr := [...]int{10, 20, 5: 1, 30, 8: 2, 40} fmt.Println(arr) // [10 20 0 0 0 1 30 0 2 40]
- Multi-dimensional arrays:
arr2d := [3][2]int{ {1, 2}, {3, 4}, {5, 6}, } fmt.Println(arr2d) // [[1 2] [3 4] [5 6]]
- Slicing an array:
Slicing allows you to create a slice from an array or an existing slice.
Syntax:arr := [5]int{0, 10, 20, 30, 40} fmt.Println(arr[1:3]) // [10 20]
slice1 := arr[2:4] // From index 2 to index 3 slice2 := arr[:4] // From the start to index 3 slice3 := arr[4:] // From index 4 to the end slice4 := arr[:] // The entire array
In Go, a slice is a dynamically-sized, flexible view into the elements of an array. Unlike arrays, slices can grow and shrink in size.
- Declaring a slice:
var slice []int // zero length
- Using the make function:
non-zero length & zero valued
slice := make([]int, 5)
- Initializing at the time of declaration:
slice := []int{1, 2, 3, 4, 5}
- Using the make function:
Extra:
- Appending to a slice:
slice := []int{1, 2, 3} slice = append(slice, 4, 5, 6) fmt.Println(slice) // [1 2 3 4 5 6]
- Removing elements from a slice:
- Removing the index 2 element:
The
...
syntax is known as the "variadic" operator. When you use it after a slice, it "unpacks" the slice so that its elements can be passed as individual arguments.slice = append(slice[:2], slice[3:]...)
- Removing the last element:
slice = slice[:len(slice)-1]
- Removing all elements expect the first two:
slice = slice[:2]
- Removing the index 2 element:
Note
When you pass a slice to a function, you are passing a reference to the underlying array, not a copy of the array's elements. Any modifications to the elements of the slice within the function will affect the original array.
func main() {
slice := []int{1, 2, 3}
fmt.Println("Before:", slice) // Before: [1 2 3]
modifySlice(slice)
fmt.Println("After:", slice) // After: [99 2 3]
}
func modifySlice(s []int) {
s[0] = 99
}
While slices behave like pass-by-reference for the data they point to, the slice itself is passed by value. This means if you reassign the slice variable inside the function, it won't affect the original slice variable outside the function.
func main() {
slice := []int{1, 2, 3}
fmt.Println("Before:", slice) // Before: [1 2 3]
reassignSlice(slice)
fmt.Println("After:", slice) // After: [1 2 3]
}
func reassignSlice(s []int) {
s = []int{100, 200, 300}
s[0] = 999
}
Important
Slices have a length, which is the number of elements they contain, and a capacity, which is the size of the underlying array they reference.
If you append elements to a slice beyond its current capacity, Go will handle this automatically by allocating a (2x) larger array and copying the existing elements to it (to the new address). This can be an expensive operation in terms of performance.
var slice []int // slice is initially nil, with a length and capacity of 0.
fmt.Println(len(slice)) // 0
fmt.Println(cap(slice)) // 0
slice = append(slice, 1, 2, 3, 4)
fmt.Println(len(slice)) // len: 4
fmt.Println(cap(slice)) // cap: 4
slice = append(slice, 5, 6)
fmt.Println(len(slice)) // len: 6
fmt.Println(cap(slice)) // cap: 8
We can also predefine the capacity of a slice when we declare it:
slice := make([]int, 0, 10)
fmt.Println(slice) // []
fmt.Println(len(slice)) // len: 0
fmt.Println(cap(slice)) // cap: 10
Caution
Never use append
on anything other than itself.
func main() {
a := make([]int, 5, 7) // 3rd argument is the capacity.
fmt.Println("a:", a) // a: [0 0 0 0 0]
b := append(a, 1)
fmt.Println("b:", b) // b: [0 0 0 0 0 1]
c := append(a, 2)
fmt.Println("a:", a) // a: [0 0 0 0 0]
fmt.Println("b:", b) // b: [0 0 0 0 0 2] <-- b got updated because of c
fmt.Println("c:", c) // c: [0 0 0 0 0 2]
}
Here, when creating the b
slice, the a
slice has a capacity of 7
and a length of 5
, which means it can add a new element without allocating a new array. So, b
now references the same array as a
. The same thing happens when creating c
. It also references the same array as a
. At this point, because both b
and c
share the same underlying array, appending 2
through c
updates the 1
that was appended through b
.
This unexpected behavior would not occur if there were not enough capacity for the new element. In that case, Go would allocate a new array and copy the existing elements to it, resulting in new addresses. But still, it is prone to go unexpected.
Maps in Go are used to store unordered collections of key-value pairs.
- Declaring & initializing a map:
You can't just declare a map with
var m map[string]int
and then assign values to it. If you try to do this, you'll get apanic: assignment to entry in nil map
error. To make the map ready to use, you need to initialize it (either empty or with values) using themake
function as shown below.m := make(map[string]int)
- Initializing with values:
m := map[string]int{ "one": 1, "two": 2, "three": 3, }
- Initializing with values:
- Accessing map elements:
fmt.Println(m["one"]) // 1
- Adding elements to a map:
m["four"] = 4 fmt.Println(m) // map[one:1 two:2 three:3 four:4]
- Deleting elements from a map:
delete(m, "two") fmt.Println(m) // map[one:1 three:3]
- Modifying map elements:
m["one"] = 100 fmt.Println(m["one"]) // 100
Extra:
-
Checking if a key exists in a map:
The optional second return value is a boolean indicating whether the key was found in the map.
_, ok := m["one"] fmt.Println(ok) // true
-
Clearing all elements from a map:
clear(m)
-
Nested maps:
m2d := make(map[string]map[string]int) m2d["a"] = map[string]int{"first": 1} fmt.Println(m2d) // map[a:map[first:1]] fmt.Println(m2d["a"]["first"]) // 1
Caution
In Go, maps need to be initialized before you can use them.
func main() {
m2d := make(map[string]map[string]int)
m2d["a"]["b"] = 1 // <-- panic: assignment to entry in nil map
m2d["a"] = make(map[string]int)
m2d["a"]["b"] = 1 // <- ok
fmt.Println(m2d["a"]["b"]) // 1
}
Generics in Go are a way to write reusable code that can work with different types. Instead of writing multiple versions of a function for different data types, you can write one generic function that works with any type.
Generics maintain type safety by allowing the compiler to check types at compile-time. This prevents many types of runtime errors.
Syntax:
func myFunc[T any](input T) T { return input }The type parameters are enclosed in square brackets
[]
and come after the function name.
- Type Parameter:
T
is a type parameter that can represent any type. You can name it anything, butT
is commonly used.- Constraint:
The
any
constraint means thatT
can be any type (any
is a shorthand forinterface{}
).
Example 1:
-
Without Generics:
func main() { fmt.Println(addInts(1, 2)) // 3 fmt.Println(addFloats(1.5, 2.3)) // 3.8 } func addInts(a, b int) int { return a + b } func addFloats(a, b float64) float64 { return a + b }
-
With Generics:
func main() { fmt.Println(add(1, 2)) // 3 fmt.Println(add(1.5, 2.3)) // 3.8 } func add[T int | float64](a T, b T) T { return a + b }
Example 2:
func main() {
fmt.Println(swap(5, 10)) // 10, 5
fmt.Println(swap(2.5, 5.2)) // 5.2, 2.5
fmt.Println(swap("Hello", "World")) // "World", "Hello"
}
func swap[T any](a, b T) (T, T) {
return b, a
}
Example 3:
func main() {
fmt.Println(swap(2.5, 5)) // 5, 2.5
fmt.Println(swap("Hello", 99)) // 99, "Hello"
}
func swap[T any, U any](a T, b U) (U, T) {
return b, a
}
Example 4:
Enforcing generic functions to work only with specific types.
type Number interface {
int | float64
}
func multiply[T Number](a, b T) T {
return a * b
}
func main() {
fmt.Println(multiply(5, 6)) // 30
fmt.Println(multiply(3.2, 4.1)) // 13.12
fmt.Println(multiply("Hello", "World")) // string does not satisfy Number (string missing in int | float64) compiler(InvalidTypeArg)
}
Goroutines are a feature in Go that allows you to run functions concurrently.
In general, we can split the execution of a program into two types of routines:
- Main Routine:
The main routine is the initial goroutine that starts when a Go program begins execution. It's the entry point of the program, defined by the main function in the main package. When the main function exits, the program terminates, so any running goroutines will also be stopped.
- Child Routines:
A child routine is any goroutine that is spawned by the main routine or other goroutines. These are created using the
go
keyword followed by a function call. Child routines run concurrently with the main routine and each other.
Example:
func main() {
go expensiveFunc("Hello")
fmt.Println("Main")
time.Sleep(1700 * time.Millisecond)
}
func expensiveFunc(text string) {
for i := 0; i < 4; i++ {
time.Sleep(500 * time.Millisecond)
fmt.Println(text, i)
}
}
Output:
Main Hello 0 Hello 1 Hello 2
The
time.Sleep
inside the main function is used to give enough time for the goroutines to finish before the main function exits. Without this, the program would exit immediately after the main routine has done its job.The output shows results for only 3 iterations, not 4 as specified in the for loop. This is because we have a
time.Sleep
of 1.7 seconds (1700 milliseconds), which is less than the minimum of 2 seconds (2000 milliseconds) needed for 4 iterations (4 * 500 ms = 2000 ms) in theexpensiveFunc
function.
Channels in Go are a way to communicate between goroutines. They are used to send and receive values between goroutines.
- Declaring a channel:
The type of a channel specifies what kind of data it can carry.
ch := make(chan int)
- Sending values to a channel:
ch <- 10
- Receiving values from a channel:
myVar := <-ch
- Closing a channel:
Closing a channel is a way to signal to the receiving goroutine that it should stop waiting for values to be sent to it. It's important to close a channel when you're done sending values to avoid a
deadlock
.close(ch)
Note
Channel synchronization ensures that communication between goroutines is properly coordinated. It guarantees that data sent between goroutines is not lost and that goroutines wait for each other when necessary, maintaining the correct order and timing of operations.
- Send Operation:
ch <- value
When a goroutine sends a value to a channel, it blocks/waits until another goroutine receives that value from the channel.
- Receive Operation:
value := <-ch
When a goroutine receives a value from a channel, it blocks until there is a value available to receive.
Example:
func main() {
ch := make(chan string)
go expensiveFunc("Hello", ch)
fmt.Println("Main")
for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}
fmt.Println("End")
}
func expensiveFunc(text string, ch chan string) {
for i := 0; i < 4; i++ {
time.Sleep(500 * time.Millisecond)
ch <- text + " " + fmt.Sprint(i)
}
}
Output:
Main Hello 0 Hello 1 Hello 2 Hello 3 End
Here, we don't need any additional mechanism in the main function to wait for the goroutines to finish. The
<-ch
operation in the main function blocks until there is a value to receive from the channel. This blocking behavior synchronizes the main function with theexpensiveFunc
goroutine. Each iteration of the loop in the main function waits for a corresponding send operation fromexpensiveFunc
. Btw, we are not forced to use a loop here. We can usefmt.Println(<-ch)
directly 4 times, one after the other, it does the same thing.In this specific example, we don't strictly need to close the channel because the main function will only receive a fixed number of messages (4 in this case) and then it stops.
Here is the modified version of it that needs to be closed explicitly:
func main() { ch := make(chan string) go expensiveFunc("Hello", ch) fmt.Println("Main") for msg := range ch { fmt.Println(msg) } fmt.Println("Done.") } func expensiveFunc(text string, ch chan string) { for i := 0; i < 4; i++ { time.Sleep(500 * time.Millisecond) ch <- text + " " + fmt.Sprint(i) } close(ch) }The
for msg := range ch { ... }
syntax essentially performs amsg := <-ch
operation under the hood, which is where the blocking behavior occurs.
The receiver of a channel can check the status of the channel using the second return value of the receive operation.
val, ok := <-ch
The second return value (ok
here) is a boolean that indicates whether the channel is closed or not.
true
: The channel is closed and no more values can be sent to it.false
: The channel is open and values can be sent to it.
func main() {
ch := make(chan int)
go func() {
ch <- 1
ch <- 2
close(ch)
}()
for {
val, ok := <-ch
if !ok {
fmt.Println("Channel is closed.")
break
}
fmt.Println("Received:", val)
}
}
When you
range
over a channel, the loop will automatically break when the channel is closed. But if you're using a manual receive loop, checkingok
helps you know when to stop receiving.
The select
statement in Go is a control structure that allows you to work with multiple channels simultaneously. It's similar to a switch
statement but is specifically designed for channel operations.
- The
select
statement listens to several channels. - It runs the first case that's ready to proceed.
- If multiple cases are ready, Go randomly picks one to execute.
- If there's no
default
case, it will block and wait until a case becomes ready. - If a
default
case is present and no other cases are ready, it executes thedefault
case immediately without blocking.
func main() {
// Preparation:
ch1 := make(chan int)
ch2 := make(chan int)
ch3 := make(chan int)
go func() {
time.Sleep(time.Second)
ch1 <- 1
}()
go func() {
time.Sleep(time.Second * 3)
ch2 <- 2
}()
go func() {
time.Sleep(time.Second * 2)
ch3 <- 3
}()
time.Sleep(time.Second * 5)
// Usage:
select {
case val1 := <-ch1:
fmt.Println(val1)
case val2 := <-ch2:
fmt.Println(val2)
case val3 := <-ch3:
fmt.Println(val3)
default:
fmt.Println("No channels are ready.")
}
fmt.Println("Done.")
}
Note
The select
statement is not a loop. It executes only one case even if multiple are ready, or it is a default
case. And then breaks the select
statement.
In Go, channels can be restricted to be either read-only
or write-only
. This is useful for defining clear communication patterns between goroutines and helps in maintaining code safety and clarity.
- Read-only channels:
A read-only channel can only be used to receive values. You cannot send values into a read-only channel.
- Write-only channels:
A write-only channel can only be used to send values. You cannot receive values from a write-only channel.
func sendData(ch chan<- int) { // ch is write-only
ch <- 42
close(ch)
}
func receiveData(ch <-chan int) { // ch is read-only
fmt.Println(<-ch)
}
func main() {
ch := make(chan int)
go sendData(ch)
receiveData(ch)
}
````