2. Go Strings
Declaring our first string
package main
import "fmt"
func main() {
str := "Hello André!"
fmt.Println(str)
}
Some languages allow strings in single
quotes, but in Go, strings are enclosed with
double quotes only.
3. Go Strings
A string is a slice of bytes
Let’s go through each index of the string and display the value at that index:
package main
import "fmt"
func main() {
str := "Hello André!"
for i := range 13 {
fmt.Print(str[i], " ")
}
fmt.Println()
}
The output doesn’t contain any characters,
just numbers:
72 101 108 108 111 32 65 110 100 114 195 169 33
These numbers represent bytes in decimal
notation. Let’s transform these bytes in
hexadecimal by using the %x verb:
4. Go Strings
package main
import "fmt"
func main() {
str := "Hello André!"
for i := range 13 {
fmt.Printf("%x ", str[i])
}
fmt.Println()
}
Output:
48 65 6c 6c 6f 20 41 6e 64 72 c3 a9 21
The first byte, 48, or 0x48 how is usually
written in hexadecimal notation represents the
letter H.
5. Go Strings
Strings are read-only
package main
import "fmt"
func main() {
str := "Hello André!"
fmt.Println(str[2]) // Even though you can do this
str[2] = byte(72) // You cannot do this
}
6. Go Strings
You can read an individual byte from a string by indexing, but you cannot change it.
This change is perfectly OK when dealing with regular slices:
package main
import "fmt"
func main() {
str := []byte("Hello André!") // Here we convert a string to a []byte
fmt.Println(str[2]) // You can do this
str[2] = byte('a') // You can fo this
fmt.Println(str) // [72 101 97 108 111 32 87 111 114 108 100 33]
}
7. Go Strings
The zero value of a string is an empty string
Regular slices can be nil. Strings in Go, even though they share a similar design with them cannot be nil:
package main
import "fmt"
func main() {
var (
sb []byte
str string
)
fmt.Println(sb == nil) // true
fmt.Println(str == "") // true
fmt.Println(str == nil) // Cannot convert 'nil' to type 'string'
_ = []byte(nil) // possible
_ = string(nil) // Cannot convert 'nil' to type 'string'
}
8. Reference Types
Pointers
Pointer is a type of variable that stores memory address (of another variable) or in other
words, points to a reference/address where a value is stored.
9. Reference Types
Every time we create a variable, its value will be stored at a specific memory address.
Since pointerToNum is (obviously) a pointer, its stored value is a memory address. Using
that memory address, we can get the value of variable num by dereferencing.
Value stored in memory
num pointerToNum
0xc00012345
0xc000401812
2 0xc00012345
Address
10. Reference Types
Modifying a variable outside of its scope
A variable can only be used within the scope where the variable is declared. Of course, we can
declare a variable in the global scope to make it (virtually) accessible everywhere. However, this
approach is not very effective if the variable is only needed in one or two functions out of many.
Using a pointer is better for this case if we truly need to modify the variable directly.
12. Reference Types
When nil is needed instead of zero values
The second case where pointers are useful is when we want a true empty value which is nil
(literally nothing) instead of the zero value (default value) of a regular type. Since the zero value
of pointer is nil, we can utilize that to mark some fields of a struct as nullable/optional (can be
empty)
13. Reference Types
type Data struct {
Name string
Score int
ExpectedScore *int
}
func showData(data Data) {
fmt.Print(data.Name, "'s score is ", data.Score, ".")
if data.ExpectedScore == nil {
fmt.Print(" ", data.Name, " didn't expect anything, though.n")
} else {
fmt.Print(" The expected score was ", *data.ExpectedScore, ".n")
}
}
15. Go Syntax: Inheritance
Inheritance means inheriting the properties of the superclass into the base class and is one of the
most important concepts in Object-Oriented Programming. Since Golang does not support classes,
so inheritance takes place through struct embedding. We cannot directly extend structs but rather
use a concept called composition where the struct is used to form other objects. So, you can say
there is No Inheritance Concept in Golang.
In composition, base structs can be embedded into a child struct and the methods of the base struct can
be directly called on the child .
Philosophy: Go favors composition over inheritance for simplicity, flexibility, and maintainability.
Struct embedding allows for code reuse, method overriding, and interface satisfaction.
This approach avoids deep hierarchies and encourages modular, decoupled design.
16. Go Syntax: Inheritance
// Base struct: Person
type
type Person struct
{
Name string
Age int
City string
}
//// Parent struct (inherits from
Person using composition)
type Parent struct
{
Person // Embedded struct
JobTitle string
}
//// Child struct (inherits from
Person using composition)
type Child struct
{
Person // Embedded struct
SchoolName string
}
17. Go Syntax: Inheritance
type first struct{
base_one string
}
type second struct{
base_two string
}
func (f first) printBase1() string{
return f.base_one
}
func (s second) printBase2() string{
return s.base_two
}
type child struct{
first
second
}
func main() {
c1 := child{
first{
base_one: "In base struct 1.",
},
second{
base_two: "nIn base struct 2.n",
},
}
fmt.Println(c1.printBase1())
fmt.Println(c1.printBase2())
}
18. Inheritance between GO & Java & C++
C++ uses class-based inheritance with virtual tables (vtable) for method overriding,
introducing a slight performance overhead due to lookup operations. Java also relies on a
Virtual Method Table (VMT) for dynamic method dispatch, but it adds additional JVM
overhead, making method calls slightly slower than C++.
Go, on the other hand, avoids vtable/VMT lookups by default, using direct function
calls for struct methods.
When interfaces are used in Go, it employs an interface table (itab), similar to vtable but
optimized.
Unlike C++’s complex multiple inheritance, Java and Go favor interfaces and
composition to achieve polymorphism.
19. Concurrency, what’s the benefit
Concurrency is the task of running and managing the multiple computations at the same
time. While parallelism is the task of running multiple computations simultaneously.
So what are some benefits:
● Faster processing. The benefit is getting tasks done faster. Imagine that you are
searching a computer for files, or processing data, if it’s possible to work on these
workloads in parallel, you end up getting the response back faster.
● Responsive apps Another benefit is getting more responsive apps. If you have an app
with a UI, imagine it would be great if you can perform some background work without
interrupting the responsiveness of the UI.
20. Goroutines
A goroutine is a lightweight thread managed by the Go runtime. What you do
is to add the keyword go in front of a function.
Here’s an example:
go Function1()
go Function2()
go Function3()
21. Imagine the following code running, what would happen?
func main() {
// call goroutine
go display("Process 1")
display("Process 2")
}
package main
import (
"fmt"
"time"
)
// create a function
func display(message string) {
fmt.Println(message)
}
Output: "Process 2"
22. Goroutines
In the above example, we have called the display() function two times:
● go display("Process 1") - as a goroutine
● display("Process 2") - regular function call
During the normal execution, the control of the program moves to the function during
the first function call and once the execution is completed, it returns back to the next
statement.
23. Goroutines
In our example, the next statement is the second call to the function. So, first Process 1 should
be printed and then Process 2.
However, we are only getting Process 2 as output.
This is because we have used go with the first function call, so it is treated as a goroutine. And
the function runs independently and the main() function now runs concurrently.
Hence, the second call is executed immediately and the program terminates without completing
the first function call.
25. Goroutines
In a concurrent program, the main() is always a default goroutine. Other goroutines can
not execute if the main() is not executing.
So, in order to make sure that all the goroutines are executed before the main function
ends, we sleep the process so that the other processes get a chance to execute.
Add this line :
time.Sleep(time.Second * 1)
Run two functions concurrently using Goroutine
26. Goroutines
// Program to illustrate multiple
goroutines
package main
import (
"fmt"
"time"
)
func display(message string) {
fmt.Println(message)
}
func main() {
// run two different goroutine
go display("Process 1")
display("Process 2")
// to sleep main goroutine for 1
sec
time.Sleep(time.Second * 1)
}
27. Goroutines
Output
Process 2
Process 1
Here, when the display("Process 2") is executing, the time.Sleep() function
stops the process for 1 second. In that 1 second, the goroutine go display("Process
1") is executed.
This way, the functions run concurrently before the main() functions stops.
28. sync.WaitGroup in Go
The sync.WaitGroup is a synchronization primitive in Go that helps manage multiple
goroutines and ensures the main program waits until all goroutines finish execution.
Problem Without WaitGroup
How sync.WaitGroup Works
1. Call wg.Add(n) → Adds n workers to wait for. Use it before launching goroutines.
2. Each goroutine calls wg.Done() → Decreases the counter by 1.
3. Main calls wg.Wait() → Blocks until all workers call Done()
29. Difference Between time.Sleep and sync.WaitGroup in Go
Both time.Sleep and sync.WaitGroup help control execution timing in concurrent Go
programs, but they serve different purposes.
time.Sleep (Forcing a Delay)
● Pauses execution for a fixed time.
● Does NOT wait for goroutines to finish.
● No synchronization between goroutines.
● Inefficient because it uses a hardcoded delay, which may be longer than necessary.
30. Benefits of Goroutines
Here are some of the major benefits of goroutines.
● With Goroutines, concurrency is achieved in Go programming. It helps two or more
independent functions to run together.
● Goroutines can be used to run background operations in a program.
● It communicates through private channels so the communication between them is
safer.
● With goroutines, we can split one task into different segments to perform better.
31. Go Channel
Channels in Go act as a medium for goroutines to communicate with each other.
We know that goroutines are used to create concurrent programs. Concurrent programs
can run multiple processes at the same time.
However, sometimes there might be situations where two or more goroutines need to
communicate with one another. In such situations, we use channels that allow goroutines
to communicate and share resources with each other.
32. Go Channel
Creating a channel
To create a channel, you need the keyword chan and the data type of the messages you are
about to send into it. Here’s an example:
ch := make(chan int)
In the above example, a channel ch will be created that accepts messages of type int.
Sending a value to a channel
To send to a channel, you need to use this operator <-, it look like a left pointing arrow and is meant to
be read as the direction something is sent. Here’s an example of sending a message to a channel:
ch <- 2
In the above code, the number 2 is sent into the channel ch.
33. Go Channel
Listening to a channel
To listen to a channel, you again use the arrow <-, but this time you need a receiving
variable on the left side and the channel on the right side, like so:
value := <- ch
34. Example: Go Channel Operations
package main
import "fmt"
func channelData
(number chan int, message chan string)
{
// send data into channel
number <- 15
message <- "Learning Go channel"
}
func main() {
// create channel
number := make(chan int)
message := make(chan string)
// function call with goroutine
go channelData(number, message)
// retrieve channel data
fmt.Println("Channel Data:", <-number)
fmt.Println("Channel Data:", <-message)
}
Output
Channel Data: 15
Channel Data: Learning Go Channel
35. Matching sending and receiving
package main
import "fmt"
func produceResults(ch chan
int) {
ch <- 1
ch <- 2
}
func main() {
ch := make(chan int)
go produceResults(ch)
var result int
result = <-ch
fmt.Println(result)
result = <-ch
fmt.Println(result)
}
Go Channel
36. Go Channel
You are invoking produceResults() and it sends messages to the channel twice:
ch <- 1
ch <- 2
in main(), you receive the results:
var result int
result = <-ch
fmt.Println(result)
result = <-ch
fmt.Println(result)
37. Go Channel
So what happens if you produce
more values than you receive like
so?
ch <- 1
ch <- 2
ch <- 3
answer: you will miss out on the
extra value.
What if it’s the opposite, you try to receive one more value
than you actually get?
var result int result = <-ch
fmt.Println(result)
result = <-ch
fmt.Println(result)
result = <-ch
fmt.Println(result)
ً What happens ?
At this point, your code will deadlock, like so: fatal error: all
goroutines are asleep - deadlock!. Your code will never
finish as that value will never arrive.
38. Go Channel
Types of Channels Present in Golang
Buffered Channel
(Synchronous
Communication)
Unbuffered Channel
(ASynchronous
Communication)
39. Go Channel
Buffered Channel
To conduct asynchronous communication,
buffered channels are required. Before receiving
any data, it could store one or more of them. We
often don't require the goroutines for this type of
channel to process send and receive operations
simultaneously. Along with a few other conditions,
received will only be blocked if there are no values
in the channel to receive, and send will only be
blocked if there is no buffer available to place the
value being sent.
When the buffer is full, sending blocks.
When the buffer is empty, receiving blocks.
Syntax:
ch := make(chan type, capacity)
Example:
buffered := make(chan int, 10)
// Buffered channel of integer type
40. Go Channel
func main() {
//Create a buffered channel
// with a capacity of 2.
ss := make(chan string, 2)
ss <- "Scaler"
ss <- "Golang channels"
fmt.Println(<-ss)
fmt.Println(<-ss)
}
Output
Scaler
Golang channels
we are using the ss variable to create a
buffered channel that has a capacity of 2,
which means it is allowed to write 2 strings
without being blocked.
41. Deadlocks in Buffered Golang Channel
in this code the write is blocked since the channel has
exceeded its capacity and program reaches deadlock
situation and print following message :
fatal error: all goroutines are asleep - deadlock!
package main
import (
"fmt"
)
func main() {
ch := make(chan string, 2)
ch <- "geeksforgeeks"
ch <- "hello"
ch <- "geeks"
fmt.Println(<-ch)
fmt.Println(<-ch)
}
42. Go Channel
Unbuffered Channel
In Go, an unbuffered channel is a channel that can be used for both sending and receiving but
doesn't have a buffer to store data. This means that when a goroutine sends data to an unbuffered
channel, the data is not stored in a buffer, but is instead immediately passed to the goroutine that
is trying to receive data from the channel. Similarly, when a goroutine receives data from an
unbuffered channel, it blocks until data is available to be received.
Unbuffered channels are synchronous, which means that a goroutine sending data to one will
block until another goroutine is prepared to accept it. A goroutine that accepts data from an
unbuffered channel will similarly block until new data becomes available.
Syntax:
Unbuffered := make(chan int) // Unbuffered channel of integer type
43. Go Channel
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 8
}()
fmt.Println(<-ch)
}
Output
8
44. Buffered VS UnBuffered channels
package main
import "fmt"
func main() {
ch := make(chan string, 2)
// Buffered channel with size 2
ch <- "Hello"
ch <- "HI"
// No blocking, as buffer has space
fmt.Println(<-ch)
fmt.Println(<-ch)
}
package main
import "fmt"
func main() {
ch := make(chan string)
// UnBuffered channel
ch <- "Hello"
ch <- "HI"
fmt.Println(<-ch)
fmt.Println(<-ch)
}
fatal error: all goroutines are asleep - deadlock!
45. Goroutines with channels
package main
import "fmt"
func sendMessage(ch chan string) {
ch <- "Hello from Goroutine!"
}
func main() {
ch := make(chan string) // Create a string channel
go sendMessage(ch) // Start goroutine
msg := <-ch // Receive message from channel
fmt.Println(msg) // Output: Hello from Goroutine!
}
46. Closing channels
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
for i := 1; i <= 3; i++ {
ch <- i
}
close(ch) // Close the channel
}()
for val := range ch { // Receives until channel is closed
fmt.Println(val)
}
}
Example: Closing and Iterating Over a Channel
47. Select Statement for Multiple Channels
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(time.Second)
ch1 <- "Message from Channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Message from Channel 2"
}()
The select statement allows a goroutine to listen to multiple channels.
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
}
48. Error Handling in Go
Error handling in Go is primarily done using the built-in error type and returning errors explicitly from
functions. Unlike languages with exceptions, Go uses a simple and explicit approach.
In Go, functions typically return an error as the last return value. If the function executes successfully, it
returns nil; otherwise, it returns an error.
49. Error Handling
1. Check Errors Immediately
When a function returns an error, check it right away instead of ignoring it. This allows for immediate
corrective action or logging, providing meaningful feedback. Ignoring errors can lead to unexpected
behaviors that are hard to debug later.
result, err := someFunction()
if err != nil {
log.Println("Error occurred:",
err)
return
}
50. Error Handling
2. Return Errors Instead of Panics (When Possible)
While panics can be useful, they should be avoided in general. Use return statements for errors where
you can handle them gracefully. Reserve panics for serious, unexpected conditions where recovery is
not possible. This keeps your code stable and prevents unexpected crashes.
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by
zero")
}
return a / b, nil
}
51. Error Handling
3. Wrap Errors for Context
Wrapping errors adds context about their origin, which is helpful during debugging. You can wrap errors
in GoLang to include additional information as they propagate through functions. At the end of the error
chain, you can unwrap them to obtain a full stack trace, offering better insight into the source of the
problem.
file, err := os.Open("file.txt")
if err != nil {
return nil, fmt.Errorf("failed to open
file: %w", err)
}
52. Error Handling
4. Use Sentinel Errors Sparingly
Sentinel errors are global definitions for common error cases. While they provide a consistent error
language, overusing them can reduce flexibility. Instead, define errors locally when relevant to a specific
part of your code, using sentinel errors only for common types you need to reference frequently.
var ErrNotFound = errors.New("resource not found")
func findResource(id string) error {
if id == "" {
return ErrNotFound
}
return nil
}
53. Error Handling
5. Leverage Custom Error Types
Custom error types allow for flexibility in defining error messages specific to certain functions or modules. These
types enable more descriptive and targeted error handling. For example, defining custom error types for database-
related errors can help detect and respond to specific issues without generalizing all errors.
type CustomError struct {
Code int
Message string
}
func (e *CustomError) Error() string {
return fmt.Sprintf("Code: %d, Message: %s",
e.Code, e.Message)
}
Editor's Notes
#15:Philosophy: Go favors composition over inheritance for simplicity, flexibility, and maintainability.
Struct embedding allows for code reuse, method overriding, and interface satisfaction.
This approach avoids deep hierarchies and encourages modular, decoupled design.
#47:Anonymous Goroutine
func() { ... }() defines an anonymous function (a function without a name).
The () at the end immediately invokes the function.