Object Oriented Programming
Structs Instead of Classes
Is Go Object Oriented?
Go is not considered a pure object-oriented programming language. However, as outlined in Go’s FAQs, it incorporates features that allow object-oriented programming to some extent.
Does Go support object-oriented programming?
Yes and no. While Go includes types, methods, and enables an object-oriented style of programming, it does not feature a traditional type hierarchy. Instead, Go utilizes the concept of “interfaces,” offering a more flexible and generalized approach. Additionally, Go supports embedding types into other types, achieving functionality similar—but not identical—to subclassing. Methods in Go are versatile and can be defined for any kind of data, including basic types like integers, not just structs (classes).
Upcoming sections will explore how Go implements object-oriented concepts, which differ in approach compared to languages like Java or C++.
Structs Instead of Classes
In Go, structs act as a substitute for classes. Structs can have associated methods, allowing data and behaviors to be bundled together in a manner similar to a class.
Let’s dive into an example to understand this concept better.
We’ll create a custom package to demonstrate how structs can effectively replace classes.
Steps to Create the Example
Set Up the Directory
- Create a subfolder in
~/Projects/
namedoop
. - Inside the
oop
directory, initialize a Go module by running:
- Create a subfolder in
go mod init oop
2. Create a Subfolder and Add Files
- Add a subfolder named
product
inside theoop
folder. - Inside
product
, create a file namedproduct.go
.
Folder Structure:
├── Projects
│ └── oop
│ ├── product
│ │ └── product.go
│ └── go.mod
3. Code for product.go
Replace the contents of product.go
with:
package product
import "fmt"
type Product struct {
Name string
Price float64
Stock int
}
func (p Product) StockValue() {
fmt.Printf("The total value of %s stock is %.2f\n", p.Name, p.Price*float64(p.Stock))
}
- The
Product
struct bundles product data:Name
,Price
, andStock
. - The
StockValue
method calculates the total value of the stock for the product.
4. Create main.go
Add a new file, main.go
, in the oop
folder.
Folder Structure:
├── Projects
│ └── oop
│ ├── product
│ │ └── product.go
│ ├── go.mod
│ └── main.go
5. Code for main.go
Replace the contents of main.go
with:
package main
import "oop/product"
func main() {
p := product.Product{
Name: "Laptop",
Price: 50000.0,
Stock: 10,
}
p.StockValue()
}
6. Run the Program
Use the following commands:
go install
oop
Output:
The total value of Laptop stock is 500000.00
The New() Function as a Constructor
The program above works fine, but what happens when a Product
struct is initialized with zero values? Modify main.go
as follows:
package main
import "oop/product"
func main() {
var p product.Product
p.StockValue()
}
Output:
The total value of stock is 0.00
Select Statement
In Go, the select
statement allows you to wait for multiple channel operations to complete, such as sending or receiving values. Similar to a switch
statement, select
lets you proceed with the first available case, making it ideal for managing concurrent operations and handling asynchronous tasks effectively.
Example
Imagine you have two tasks that finish at different times. You can use select
to receive data from whichever task completes first.
package main
import (
"fmt"
"time"
)
func task1(ch chan string) {
time.Sleep(2 * time.Second)
ch <- "Task 1 finished"
}
func task2(ch chan string) {
time.Sleep(4 * time.Second)
ch <- "Task 2 finished"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go task1(ch1)
go task2(ch2)
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
Output:
Task 1 finished
In this example, “Task 1 finished” will be printed after 2 seconds, as task1
finishes before task2
. If task2
had completed first, the output would have been “Task 2 finished.”
Syntax
The select
statement in Go listens to multiple channel operations and proceeds with the first ready case.
select {
case value := <-channel1:
// Executes if channel1 is ready to send/receive
case channel2 <- value:
// Executes if channel2 is ready to send/receive
default:
// Executes if no other case is ready
}
Key Points:
- The
select
statement waits until at least one channel operation is ready. - If multiple channels are ready, one is selected at random.
- The
default
case is executed if no other case is ready, preventing the program from blocking.
Select Statement Variations
Basic Blocking Behavior: In this variation, we modify the example to remove the select
statement and see the blocking behavior when no channels are ready.
package main
import (
"fmt"
)
func main() {
ch := make(chan string)
select {
case msg := <-ch:
fmt.Println(msg)
default:
fmt.Println("No channels are ready")
}
}
Output:
No channels are ready
Handling Multiple Cases: If multiple tasks are ready at the same time, select
chooses one case randomly. This can occur if tasks have nearly the same completion times.
Example:
package main
import (
"fmt"
"time"
)
func portal1(channel1 chan string) {
time.Sleep(3 * time.Second)
channel1 <- "Welcome from portal 1"
}
func portal2(channel2 chan string) {
time.Sleep(9 * time.Second)
channel2 <- "Welcome from portal 2"
}
func main() {
R1 := make(chan string)
R2 := make(chan string)
go portal1(R1)
go portal2(R2)
select {
case op1 := <-R1:
fmt.Println(op1)
case op2 := <-R2:
fmt.Println(op2)
}
}
Output:
Welcome from portal 1
Using Select with Default Case to Avoid Blocking: The default
case can be used to avoid blocking when no cases are ready. Here’s an example of modifying the structure to include the default
case.
package main
import (
"fmt"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
default:
fmt.Println("No tasks are ready yet")
}
}
Output:
No tasks are ready yet
Infinite Blocking without Cases: A select
statement with no cases will block indefinitely. This is commonly used when you need an infinite wait.
package main
func main() {
select {} // This blocks indefinitely because no cases are present
}
Output:
Welcome to Go Programming
Multiple Goroutines
A Goroutine is essentially a function or method that operates independently and concurrently with other Goroutines in a program. In simpler terms, any concurrently executing task in the Go programming language is referred to as a Goroutine. The Go language provides the capability to create multiple Goroutines within a single program. A Goroutine can be initiated by prefixing the go
keyword to a function or method call, as demonstrated in the syntax below:
func functionName() {
// Statements
}
// Initiating a Goroutine by prefixing the function call with the go keyword
go functionName()
Example: Managing Multiple Goroutines in Go
// Go program demonstrating Multiple Goroutines
package main
import (
"fmt"
"time"
)
// For the first Goroutine
func displayNames() {
names := [4]string{"Alice", "Bob", "Charlie", "Diana"}
for i := 0; i < len(names); i++ {
time.Sleep(200 * time.Millisecond)
fmt.Printf("%s\n", names[i])
}
}
// For the second Goroutine
func displayAges() {
ages := [4]int{25, 30, 35, 40}
for j := 0; j < len(ages); j++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%d\n", ages[j])
}
}
// Main function
func main() {
fmt.Println(">>> Main Goroutine Starts <<<")
// Initiating the first Goroutine
go displayNames()
// Initiating the second Goroutine
go displayAges()
// Allowing time for Goroutines to complete
time.Sleep(2500 * time.Millisecond)
fmt.Println("\n>>> Main Goroutine Ends <<<")
}
Output:
>>> Main Goroutine Starts <<<
Alice
25
Bob
Charlie
30
Diana
35
40
>>> Main Goroutine Ends <<<
The first portion of the image represents the numbers Goroutine, the second portion represents the alphabets Goroutine, the third portion represents the main Goroutine and the final portion in black merges all the above three and shows us how the program works. The strings like 0 ms, 250 ms at the top of each box represent the time in milliseconds and the output is represented in the bottom of each box as 1, 2, 3 and so on. The blue box tells us that 1 is printed after 250 ms, 2 is printed after 500 ms and so on. The bottom of the last black box has values 1 a 2 3 b 4 c 5 d e main terminated which is the output of the program as well. The image is self-explanatory and you will be able to understand how the program works.

Channels in Go Language
A channel in Go is a medium that enables communication between goroutines without using explicit locks. Channels facilitate the exchange of data between goroutines in a synchronized manner, and by default, they are bidirectional. This means the same channel can be used for both sending and receiving data. Below is a detailed explanation and examples of how channels work in Go.
Creating a Channel; In Go, you use the chan
keyword to create a channel. A channel can only transport data of a specific type, and you cannot use the same channel to transfer different data types.
Syntax:
var channelName chan Type
Example:
// Go program to demonstrate channel creation
package main
import "fmt"
func main() {
// Creating a channel using var
var myChannel chan string
fmt.Println("Channel value:", myChannel)
fmt.Printf("Channel type: %T\n", myChannel)
// Creating a channel using make()
anotherChannel := make(chan string)
fmt.Println("Another channel value:", anotherChannel)
fmt.Printf("Another channel type: %T\n", anotherChannel)
}
Output:
Channel value: <nil>
Channel type: chan string
Another channel value: 0xc00007c060
Another channel type: chan string
Sending and Receiving Data in a Channel
Channels in Go operate through two primary actions: sending and receiving, collectively referred to as communication. These operations use the <-
operator to indicate the direction of the data flow.
1. Sending Data: The send operation transfers data from one goroutine to another via a channel. For basic data types like integers, floats, and strings, sending is straightforward and safe. However, when working with pointers or references (like slices or maps), ensure that only one goroutine accesses them at a time.
myChannel <- value
2. Receiving Data: The receive operation fetches the data from a channel that was sent by another goroutin
var channelName chan Type
Example:
// Go program to demonstrate send and receive operations
package main
import "fmt"
func calculateSquare(ch chan int) {
num := <-ch
fmt.Println("Square of the number is:", num*num)
}
func main() {
fmt.Println("Main function starts")
// Creating a channel
ch := make(chan int)
go calculateSquare(ch)
ch <- 12
fmt.Println("Main function ends")
}
Output:
Main function starts
Square of the number is: 144
Main function ends
Closing a Channel: You can close a channel using the close()
function, which indicates that no more data will be sent to that channel.
Syntax:
close(channelName)
When iterating over a channel using a for range
loop, the receiver can determine if the channel is open or closed.
Syntax:
value, ok := <-channelName
If ok
is true
, the channel is open, and you can read data. If ok
is false
, the channel is closed.
Example:
// Go program to close a channel using for-range loop
package main
import "fmt"
func sendData(ch chan int) {
for i := 1; i <= 5; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go sendData(ch)
for value := range ch {
fmt.Println("Received value:", value)
}
fmt.Println("Channel closed")
}
Output:
Received value: 1
Received value: 2
Received value: 3
Received value: 4
Received value: 5
Channel closed
Key Points to Remember
Blocking Behavior:
Sending data blocks until another goroutine is ready to receive it.
Receiving data blocks until another goroutine sends it.
Nil Channels:
A zero-value channel is
nil
, and operations on it will block indefinitely.
Iterating Over Channels:
A
for range
loop can iterate over the values in a channel until it’s closed.
Example:
// Go program to iterate over channel using for-range
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "Hello"
ch <- "World"
ch <- "Go"
close(ch)
}()
for msg := range ch {
fmt.Println(msg)
}
}
Output:
Hello
World
Go
Channel Properties
1.Length of a Channel: Use len()
to find the number of elements currently in the channel.
Example:
// Go program to find channel length
package main
import "fmt"
func main() {
ch := make(chan int, 3)
ch <- 10
ch <- 20
fmt.Println("Channel length:", len(ch))
}
Output:
Channel length: 2
2. Capacity of a Channel: Use cap()
to find the total capacity of the channel.
Example:
// Go program to find channel capacity
package main
import "fmt"
func main() {
ch := make(chan float64, 4)
ch <- 1.1
ch <- 2.2
fmt.Println("Channel capacity:", cap(ch))
}
Output:
Channel capacity: 4
Unidirectional Channel in Golang
In Golang, channels act as a communication mechanism between concurrently running goroutines, allowing them to transmit and receive data. By default, channels in Go are bidirectional, meaning they support both sending and receiving operations. However, it’s possible to create unidirectional channels, which can either exclusively send or receive data. These unidirectional channels can be constructed using the make()
function as demonstrated below:
// For receiving data only
c1 := make(<-chan bool)
// For sending data only
c2 := make(chan<- bool)
Example:
// Go program to demonstrate the concept
// of unidirectional channels
package main
import "fmt"
func main() {
// Channel restricted to receiving data
recvOnly := make(<-chan int)
// Channel restricted to sending data
sendOnly := make(chan<- int)
// Display the types of the channels
fmt.Printf("%T", recvOnly)
fmt.Printf("\n%T", sendOnly)
}
Output:
<-chan int
chan<- int
Converting Bidirectional Channels into Unidirectional Channels
In Go, you can convert a bidirectional channel into a unidirectional channel, meaning you can restrict it to either sending or receiving data. However, the reverse conversion (from unidirectional back to bidirectional) is not possible. This concept is illustrated in the following example:
Example:
// Go program to illustrate conversion
// of a bidirectional channel into a
// unidirectional channel
package main
import "fmt"
// Function to send data through a send-only channel
func sendData(channel chan<- string) {
channel <- "Hello from Golang"
}
func main() {
// Creating a bidirectional channel
bidiChannel := make(chan string)
// Passing the bidirectional channel to a function,
// which restricts it to a send-only channel
go sendData(bidiChannel)
// Receiving data from the channel
fmt.Println(<-bidiChannel)
}
Output:
Hello from Golang