SlideShare a Scribd company logo
SWAPD 351
Software Architecture and Design
Lab 02: Advanced Go Concepts
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.
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:
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.
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
}
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]
}
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'
}
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.
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
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.
Reference Types
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)
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")
}
}
Reference Types
func main() {
expected := 100
shaneData := Data{
Name: "Shane",
Score: 88,
ExpectedScore: &expected,
}
enahsData := Data{
Name: "Enahs",
}
showData(shaneData)
showData(enahsData)
}/* Output
Shane's score is 88. The
expected score was 100.
Enahs's score is 0. Enahs didn't
expect anything, though.
*/
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.
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
}
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())
}
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.
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.
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()
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"
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.
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.
Goroutines
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
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)
}
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.
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()
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.
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.
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.
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.
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
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
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
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)
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.
Go Channel
Types of Channels Present in Golang
Buffered Channel
(Synchronous
Communication)
Unbuffered Channel
(ASynchronous
Communication)
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
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.
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)
}
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
Go Channel
package main
import "fmt"
func main() {
ch := make(chan int)
go func() {
ch <- 8
}()
fmt.Println(<-ch)
}
Output
8
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!
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!
}
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
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)
}
}
}
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.
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
}
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
}
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)
}
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
}
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)
}

More Related Content

PDF
Let's golang
PDF
Golang and Eco-System Introduction / Overview
PDF
Introduction to Python Programming | InsideAIML
PDF
Functions
PPTX
Python functions PYTHON FUNCTIONS1234567
PPTX
golang_getting_started.pptx
PPTX
PPt Revision of the basics of python1.pptx
PDF
Go Lang Tutorial
Let's golang
Golang and Eco-System Introduction / Overview
Introduction to Python Programming | InsideAIML
Functions
Python functions PYTHON FUNCTIONS1234567
golang_getting_started.pptx
PPt Revision of the basics of python1.pptx
Go Lang Tutorial

Similar to Lab2_AdvancedGoConceptswithgo_foundationofgolang_.pptx (20)

PDF
Python basic
PPTX
Dti2143 chapter 5
PPTX
Go Programming Language (Golang)
PPTX
Introduction to Python Programming
PPTX
Golang basics for Java developers - Part 1
PDF
Something about Golang
PDF
Geeks Anonymes - Le langage Go
PPTX
Working with functions.pptx. Hb.
PPTX
Unit2 input output
PDF
Introduction to go language programming
PDF
PPE-Module-1.2 PPE-Module-1.2 PPE-Module-1.2.pdf
PPTX
functions
PPTX
Python1_Extracted_PDF_20250404_1054.pptx
PPTX
C programming language tutorial
PDF
Python-Cheat-Sheet.pdf
PDF
Introduction to go
PDF
Go ahead, make my day
PPTX
System Calls.pptxnsjsnssbhsbbebdbdbshshsbshsbbs
PPTX
C function
PPTX
CHAPTER 01 FUNCTION in python class 12th.pptx
Python basic
Dti2143 chapter 5
Go Programming Language (Golang)
Introduction to Python Programming
Golang basics for Java developers - Part 1
Something about Golang
Geeks Anonymes - Le langage Go
Working with functions.pptx. Hb.
Unit2 input output
Introduction to go language programming
PPE-Module-1.2 PPE-Module-1.2 PPE-Module-1.2.pdf
functions
Python1_Extracted_PDF_20250404_1054.pptx
C programming language tutorial
Python-Cheat-Sheet.pdf
Introduction to go
Go ahead, make my day
System Calls.pptxnsjsnssbhsbbebdbdbshshsbshsbbs
C function
CHAPTER 01 FUNCTION in python class 12th.pptx
Ad

Recently uploaded (20)

PPTX
history of c programming in notes for students .pptx
PDF
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
PPTX
VVF-Customer-Presentation2025-Ver1.9.pptx
PPTX
Operating system designcfffgfgggggggvggggggggg
PPTX
Introduction to Artificial Intelligence
PPTX
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
PDF
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards — Omne...
PPTX
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
PDF
How to Choose the Right IT Partner for Your Business in Malaysia
PPTX
Online Work Permit System for Fast Permit Processing
PDF
How to Migrate SBCGlobal Email to Yahoo Easily
PDF
Design an Analysis of Algorithms I-SECS-1021-03
PDF
Which alternative to Crystal Reports is best for small or large businesses.pdf
PPTX
CHAPTER 2 - PM Management and IT Context
PPTX
Transform Your Business with a Software ERP System
PPTX
ManageIQ - Sprint 268 Review - Slide Deck
PPTX
CHAPTER 12 - CYBER SECURITY AND FUTURE SKILLS (1) (1).pptx
PDF
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
PDF
medical staffing services at VALiNTRY
PDF
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
history of c programming in notes for students .pptx
Claude Code: Everyone is a 10x Developer - A Comprehensive AI-Powered CLI Tool
VVF-Customer-Presentation2025-Ver1.9.pptx
Operating system designcfffgfgggggggvggggggggg
Introduction to Artificial Intelligence
Agentic AI : A Practical Guide. Undersating, Implementing and Scaling Autono...
Audit Checklist Design Aligning with ISO, IATF, and Industry Standards — Omne...
Lecture 3: Operating Systems Introduction to Computer Hardware Systems
How to Choose the Right IT Partner for Your Business in Malaysia
Online Work Permit System for Fast Permit Processing
How to Migrate SBCGlobal Email to Yahoo Easily
Design an Analysis of Algorithms I-SECS-1021-03
Which alternative to Crystal Reports is best for small or large businesses.pdf
CHAPTER 2 - PM Management and IT Context
Transform Your Business with a Software ERP System
ManageIQ - Sprint 268 Review - Slide Deck
CHAPTER 12 - CYBER SECURITY AND FUTURE SKILLS (1) (1).pptx
Flood Susceptibility Mapping Using Image-Based 2D-CNN Deep Learnin. Overview ...
medical staffing services at VALiNTRY
T3DD25 TYPO3 Content Blocks - Deep Dive by André Kraus
Ad

Lab2_AdvancedGoConceptswithgo_foundationofgolang_.pptx

  • 1. SWAPD 351 Software Architecture and Design Lab 02: Advanced Go Concepts
  • 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") } }
  • 14. Reference Types func main() { expected := 100 shaneData := Data{ Name: "Shane", Score: 88, ExpectedScore: &expected, } enahsData := Data{ Name: "Enahs", } showData(shaneData) showData(enahsData) }/* Output Shane's score is 88. The expected score was 100. Enahs's score is 0. Enahs didn't expect anything, though. */
  • 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.