If you want to learn a language that is fast, efficient, and follows modern infrastructure, a language that will help you work on the backend seamlessly while handling concurrency without any complexity, you might be wondering if such a language exists. Well, to answer your question, the answer is a big yes, and that language is Golang.
Golang is Developed by Google, and it was open-source in 2009. Golang is a server-side language that helps us create web applications and also it is used in microservice architecture. Golang uses minimal resources while providing speed. As a compiled language, Golang is platform-independent.
The infrastructure has evolved over time, with multi-core processors becoming common and cloud infrastructure spreading rapidly. As a result, today's infrastructure is highly scalable and distributed. However, existing languages did not fully leverage these advancements. For instance, many languages struggled to achieve multitasking despite the capabilities of the infrastructure.
Installation and Hello World
Install Go on your machine using the official documentation.
Here is a simple Golang "Hello, World!" program:
package main //package declaration
import "fmt" // import fmt package
func main() {
fmt.Println("Hello, World!")
}
Let's break it down:
At the top, we declare a main
package - In Golang packages are collections of source files that are compiled together.
We import the fmt
(format) package - fmt
is a built-in package with printing functions.
The main
function is where execution begins.
We call Println()
from fmt
to print "Hello, world!"
You can run the program with the command go run filename
Commands
Go Modules
Go modules are a simple way to manage dependencies in Go projects. They ensure that your project has the required packages and their specific versions, making it easy to share and collaborate with others. Here are some essential commands:
go mod init
: Initializes a new module and creates ago.mod
file that defines the modules used in the project.go mod tidy
: Adds any missing modules necessary to build the current module and its dependencies. It also removes any unused modules that do not contribute relevant packages. Thego.mod
andgo.sum
files are updated accordingly.go mod verify
: Verifies that the downloaded dependencies have the expected content, ensuring the integrity of your project.go list -m all
: Lists all modules required to build the current module, along with indirect and test dependencies.go get
: Adds dependencies to the current module and installs them.go list -m -versions
: Lists all available versions of a module.go mod edit -go version
: Updates the Go version directive in thego.mod
file to the specified version.go mod vendor
: Copies all project dependencies into a "vendor" directory for better control over the project's dependencies.
Using Go modules, you can easily manage your project's dependencies and ensure a smooth development process with just a few straightforward commands.
Sum (go.sum)
The go.sum
file contains cryptographic checksums or hashes of the content of specific module versions. A module, in this context, refers to a collection of related Go packages. The cryptographic checksums act like unique fingerprints for the content of each module version.
Go Build
We can build a program using the following command:
$ go build main.go
we can also build for different platforms using the following command:
$ GOOS=linux GOARCH=amd64 go build main.go
Fundamentals of Golang
Packages
A package is just a collection of source files in the same directory that are compiled together. here are some predefined packages you can use in the project.
fmt
- This package handles formatted input and output operations. It includes functions likePrintf
andScanf
, which allows you to display and read data in a specific format.os
- Theos
package provides an interface to interact with the operating system in a platform-independent manner. It enables tasks like file operations, environment variables, and process management.strconv
- This package deals with converting data to and from string representations for basic data types likeintegers
andfloats
.time
- Thetime
package offers functionality for working with time, allowing you to measure and display time durations, delays, and formatting time values.math
- Themath
package includes essential mathematical constants and functions for various mathematical calculations.net/http
- This package provides implementations for creating HTTP clients and servers, making it easier to build web applications and interact with web services.encoding/json
- With this package, you can encode Go data structures into JSON format and decode JSON back into Go data structures.
Variables
Variables are used to store and represent data in computer programs. They hold values that can change during the execution of a program. Here are some important ways to specify a variable in Go:
var
: Thevar
keyword is used to declare variables with initial values.const
: Theconst
keyword is used to declare constants.:=
: short variable declaration. Can be used only inside a function. Called Syntax sugar. We can't use it to declare a global variable.Global Variables : Variables declared outside of any function or block are known as global variables
Print statements
There are several ways to print in Go. We have to use the fmt
package to print anything.
fmt.Println
- It prints a line.fmt.Print
- It prints without a new line.fmt.Printf
- It prints with formatting.
Placeholders for Printf
%v
: Value in the default format.%+v
: Value in the default format with field names.%T
: The type of value.%t
: Boolean value.%d
: Decimal integer value.%b
: Binary integer value.%c
: Character value.%x
: Hexadecimal integer value.%f
: Floating-point number value.%s
: String value.
What is Pass-By-Value?
In Go, when you pass a parameter to a function by value, it means that a duplicate of the parameter's value is created and stored in a separate memory location. This ensures that any modifications made to the parameter within the function only affect the copied value and not the original one.
Go's primitive or basic types such as int, float, boolean, string, array, and struct are all passed by value. This means that when you call a function and provide these types as arguments, the function receives copies of these values rather than direct access to the original ones.
Passing by value is the common way of sending values to functions in Go, and it helps maintain data integrity by preventing unintended changes to the original data outside the function scope.
Data Types
When we declare a value we don't need to specify the type. Go will infer the type from the value. But when we declare without a value we need to specify the type.
bool
- it can be either true or false. Example:false
string
- a string of characters. Example:"Gojo"
int
- integer number. Example:156
float64
- floating point number. Example:3.38
uint
- Unlike signed integers, unsigned integers only contain positive numbers.When we declare a variable without a value, Go will assign a default value to it. Eg:
0
forint
,false
forbool
etc.
Scan
fmt.Scan
is a function in Go that reads text from the standard input (keyboard), and it scans the input text into successive arguments. The function considers newlines as spaces. After scanning, it returns the number of items successfully read. If the number of successfully scanned items is less than the number of arguments provided, an error will be reported to explain the reason.
Usage:
To use fmt.Scan
, you can provide variables where you want to store the input values. For example:
var name string
numScanned, err := fmt.Scan(&name)
In this example, the &name
refers to the memory address of the variable name
, where the scanned input text will be stored. The variable numScanned
will contain the number of items successfully scanned, and if there's any issue during scanning, the error will be stored in the err
variable.
Scanning through bufio
The bufio
package in Go implements a buffered reader, which is valuable for its efficiency when dealing with numerous small reads. It provides additional reading methods that enhance its usefulness.
The bufio
package in Go is used to enhance reading and writing efficiency by providing buffered I/O operations. It introduces a buffered reader, which means it reads data from an input source and stores it in a buffer. When you read from the buffer, it's faster and more efficient than reading directly from the source.
The primary benefit of using bufio
arises when you need to perform many small reads. Reading data in small chunks can be less efficient due to the overhead of system calls and data transfers. By using the buffered reader from the bufio
package, you can minimize these overheads and improve overall performance.
The package offers various methods for reading different data types, like ReadString()
, ReadBytes()
, ReadLine()
, etc., making it convenient to parse and process data in different formats.
In summary, bufio
in Go provides a simple and effective way to optimize reading operations, especially when dealing with numerous small reads, making it a valuable addition to any I/O-intensive application.
"variable, err syntax"
In the above example, we used variable, and err syntax. It is a common way to handle errors in Go. If we don't want to handle the error we can use _
to ignore it.
text, err := reader.ReadString('\n') // if you don't want to handle err you can use _ to ignore
if err != nil{
panic(err)
}
panic
- It is a built-in function that stops the ordinary flow of control and begins panicking. When the function calls panic, the execution of the function stops, any deferred functions in that function are executed normally, and then the function returns to its caller.
Conversion
We can convert a value from one type to another. We can use strconv
package to convert a data type to another.
package main
import (
"fmt"
"strconv"
)
func main() {
a := strconv.Itoa(12)
fmt.Printf("%q\n", a)
}
// Output : "12"
Time
We can use time
package to get the current time time.Now()
Pointers
A pointer is a variable that stores the memory address of another variable. We can declare a pointer by using *
operator. Eg:
var p *int // int - the type int is a pointer to an int.
// We can get the memory address of a variable using & operator.
var name = "John" fmt.Println(&name) // it will print the memory address of the variable. name
var name = "John"
var myName = &name
// We can get the value of a pointer using * operator. Called dereferencing.
fmt.Println(*myName) // it will print the value of the variable name
Arrays
An array is a numbered sequence of elements of a single type with a fixed length. We can store a fixed-size collection of elements of the same type.
We declare an array as follows:
var arr [5]int // array of 5 integers
arr[0] = 1 arr[1] = 2
Slice
In this we don't need to specify the size of the array. It is a dynamically sized, flexible view of the elements of an array.
Slice of integers: var names []int
Unlink arrays, we don't add elements to a slice using arr[index] = value.
We use the append function to add elements to a slice.
names = append(names, 1)
names = append(names, 2)
Loops
In Go, there is only one looping construct, the for loop.
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// range - The range form of the for loop iterates over a slice or map.
names := []string{
"ken",
"ronaldo",
"son",
"messi"
}
for i, name := range names {
fmt.Println(i, name)
}
// _ - The blank identifier is a special identifier that is used to ignore values when multiple values are returned from a function.
// It is used to ignore the index in the above example.
for _, name := range names {
fmt.Println(name)
}
Conditions
If-else
The if-else
statement allows you to conditionally execute a block of code. If the given condition is true, the code inside the if block is executed; otherwise, the code inside the else block is executed.
if num > 0 { // this condition will only be true if num is greater than 0
fmt.Println("Positive")
} else if num == 0 { // this condition will only be true if num is equal to 0
fmt.Println("Equal to zero")
} else { // this condition will only be true if num is less than 0
fmt.Println("Negative")
}
Break and Continue
break Statement:**
The break
statement is used to terminate a loop
or switch
statement, and it transfers the execution to the statement immediately following the loop
or switch
.
for i := 0; i < 5; i++ {
if i == 3 {
break
}
fmt.Println(i)
}
continue statement:
The continue
statement terminates the current iteration of the loop
, and resumes execution at the next iteration. It can be used only within an iterative or switch
statement and only within the body of that statement.
for i := 0; i < 5; i++ {
if i == 3 {
continue
}
fmt.Println(i)
}
Switch
The switch
statement is a shorter way to write a sequence of if-else
statements. It runs the first case whose value is equal to the condition expression.
switch num {
case 1:
fmt.Println("One")
fallthrough // it will execute the next case
case 2:
fmt.Println("Two")
default:
fmt.Println("Other")
}
here, we don't need to write break
after each case. It will automatically break after each case. If we want to execute the next case we can use fallthrough
keyword.
Functions
A function is a block of code that performs a specific task, making it a reusable piece of code.
func add(x int, y int) int {
// We can specify the type of the parameters
return x + y
}
func main() {
fmt.Println(add(1, 2))
// We can call the function by passing the arguments
}
A function is capable of returning multiple values:
func swap(x, y string) (string, string) {
// When we have more than one return value, we put them in parentheses ()
return y, x
}
a, b := swap("hello", "world")
// We can get the return values using multiple assignment
Anonymous Functions
An anonymous function is a function declared without a name. Such functions are called anonymous because they lack an explicit identifier.
func add(x, y int) int {
return x + y
}
In the example above, we define an anonymous function that takes two integer parameters x
and y
, and it returns their sum. Anonymous functions are particularly useful when we need to pass a function as an argument to another function, like in the case of higher-order functions or when we want to define short, one-off functions inline.
Methods
A method is a function with a special receiver argument. The receiver appears in its argument list between the func
keyword and the method name.
type Person struct {
name string
}
func (p Person) getName() string { // We can use the receiver argument to access the fields of the struct
return p.name
}
func main() {
p := Person{name: "John"}
fmt.Println(p.getName())
}
In this code snippet, we have defined a Person
struct with a single field name
. The getName()
method is associated with the Person
struct and allows us to access the name
field of the struct using the receiver p
. The main()
function demonstrates how to create a Person
instance and call the getName()
method to print the person's name.
Defer
A defer
statement in Go is used to delay the execution of a function until the surrounding function returns.
Here's an example:
func main() {
defer fmt.Println("world") // It will print "world" after the main function returns
fmt.Println("hello")
}
In this code snippet, the fmt.Println("world")
function call is deferred until the end of the main()
function's execution. As a result, "hello" will be printed first, and then "world" will be printed after the main()
function has been completed and is about to return.
Mutex
A Mutex
is a mutual exclusion lock used for synchronization in concurrent programming. The zero value for a Mutex represents an unlocked mutex.
var mutex sync.Mutex
mutex.Lock() // Locks the mutex
mutex.Unlock() // Unlocks the mutex
RWMutex:
An RWMutex is a reader/writer mutual exclusion lock that allows multiple readers or a single writer at a time. When a writer is active, no readers can be active.
var rwMutex sync.RWMutex
rwMutex.RLock() // Locks the mutex for reading
rwMutex.RUnlock() // Unlocks the mutex for reading
rwMutex.Lock() // Locks the mutex for writing
rwMutex.Unlock() // Unlocks the mutex for writing
Package-Level Variables
In Go, we can declare variables at the package level, making them accessible to all the functions within the package. These variables are known as package level variables.
NOTE: The short variable declaration operator (:=) cannot be used to declare package level variables.
var name string = "ken" // Package level variable
func main() {
fmt.Println(name) // Accessing the package level variable
}
By using package-level variables, we can conveniently share data among multiple functions within the package.
Exporting and Importing
To make functions and variables available for use in other packages, we need to export them. The standard convention is to capitalize the first letter of the function/variable name, which signifies its public accessibility.
For instance, let's consider the following example:
// mathematics.go
package mathematics
//Add is a public function that takes two integers and returns their sum
func Add(x, y int) int {
return x + y
}
// Name is a public variable containing the name "aditya"
var Name string = "aditya"
Now, to use the Add
function and the Name
variable in another package, we can import the mathematics
package:
// main.go
package main
import (
"fmt"
"module_name/mathematics"
)
func main() {
result := mathematics.Add(5, 3)
fmt.Println("Result:", result)
fmt.Println("Name:", mathematics.Name)
}
By adopting this approach, we can maintain a cleaner and more organized codebase, promoting code reuse and making it easier to collaborate with other developers.
Scope Rules
In programming, scope rules dictate the visibility and accessibility of variables within a codebase. There are three main levels of scope:
Local Variables: These variables are confined to the function in which they are declared. As a result, they are not visible or accessible outside that specific function.
Package Level Variables: Variables declared at the package level have a broader scope. They are visible and accessible to all the functions within the same package.
Exported Variables: Exported variables, like package-level variables, are scoped to the package in which they are declared. However, they have extended visibility. Not only are they accessible within the package itself, but they can also be accessed by other packages that import the package containing the exported variables.
In summary, the scope rules control how variables can be accessed throughout the codebase, ensuring proper encapsulation and preventing unintended variable collisions or modifications.
Maps
A map is an unordered collection of key-value pairs. One key advantage of maps is their flexibility; they can store key-value pairs of any type. However, a limitation to consider is that all keys must be of the same type, and all values must also be of the same type.
To create a map in Go, you use the following syntax:
var cars = make(map[string]string) // map of string to string
In this example, we create a map that can store key-value pairs where both the keys and values are of type string.
To add a key-value pair to the map, you can use the following syntax:
football["Ronaldo"] = "UCL" // adding a key-value pair
This code adds the key "Ronaldo" with the value "UCL" to the football
map.
To delete a key-value pair from the map, you can use the delete
function:
delete(football, "Ronaldo") // deleting a key-value pair
This line of code removes the key-value pair with the key "Ronaldo" from the football
map.
Remember that in Go, you can use various types for both keys and values in a map, making it a versatile data structure for many use cases.
Structs
A struct is a collection of fields, forming a data structure that allows us to bundle together related data and behaviour. Structs are commonly used to represent real-world objects and can handle multiple data types. They are analogous to classes in other programming languages.
Declaring a Struct:
To define a struct in Go, we use the type
keyword followed by the name of the struct, and then list the fields along with their data types:
type Person struct {
name string
age int
}
In this example, we have defined a Person
struct with two fields: name
of type string
and age
of type int
.
Structs are incredibly useful in Go for creating custom data types that encapsulate both data and behaviour. They allow us to organize our code efficiently and build complex data structures to represent various entities within our programs.
Go Routines
A goroutine is a lightweight thread managed by the Go runtime. We can create a goroutine using the keyword go
. It is analogous to threads in other programming languages.
The purpose of a goroutine is to execute a function concurrently with other functions. It allows the function to run independently, not waiting for its completion before continuing with the rest of the program.
Example of creating a goroutine:
go func() {
fmt.Println("Hello")
}()
If the main function exits, the program will terminate immediately, even if the goroutine is still running. To prevent this, we can use the WaitGroup
type. Using the Wait
method, the main function will wait until all the goroutines are marked as "done" with wg.Done()
have completed their execution before exiting the program.
WaitGroup
The WaitGroup is a useful synchronization primitive in Go. It allows you to wait for a collection of goroutines to finish their execution before proceeding further.
var wg sync.WaitGroup
func main() {
wg.Add(1) // We are adding 1 to the WaitGroup
go sayHello()
wg.Wait() // We are waiting for the WaitGroup to become zero
}
func sayHello() {
fmt.Println("Hello")
wg.Done() // We are decrementing the WaitGroup by 1
}
First, we declare a
WaitGroup
variablewg
from thesync
package.In the
main
function, we add 1 to theWaitGroup
usingwg.Add(1)
, indicating that one goroutine is about to be launched.We then call
go sayHello()
to execute thesayHello
function concurrently as a separate goroutine.Next, we use
wg.Wait()
to block the execution of the main goroutine until all the goroutines in theWaitGroup
have finished executing.Inside the
sayHello
function, we print "Hello" to the console usingfmt.Println("Hello")
.Finally, we use
wg.Done()
to indicate that thesayHello
goroutine has finished, effectively decrementing theWaitGroup
counter by 1.
The
goto
statement is not used in the code provided. It is worth mentioning that thegoto
statement is rarely used in Go, and it is generally considered bad practice as it can lead to difficult-to-read and maintain code. Instead, Go encourages the use of structured control flow with loops and conditional statements.
rand
rand.Seed()
is a function used to initialize the default random number generator Source to a deterministic state. If Seed()
is not called, the generator behaves as if it were seeded by Seed(1)
. It should be called only once and is typically invoked before the first call to Intn()
or Float64()
.
To make use of the current time as the seed value, you can call rand.Seed(
time.Now
().UnixNano())
. This ensures that each time you run the program, the random number generator will be initialized with a different seed based on the current time, making the generated random numbers more unpredictable.
JSON
You can utilize the json
package in Go to efficiently encode and decode JSON data.
json.Marshal()
: This function is employed to encode a value into JSON format. It returns a byte slice containing the JSON-encoded data and an error, if any.json.MarshalIndent()
: Similarly, this function encodes a value to JSON, but with the added benefit of indentation for improved readability. It returns a byte slice representing the indented JSON data along with an error, if any.json.Unmarshal()
: To decode a JSON-encoded value back into its original Go representation, you can use this function. It returns an error if the decoding process encounters any issues.
In Go's structs
, you have the flexibility to utilize the json
tag. This tag allows you to specify the desired name of the field when serialized to JSON. By utilizing this tag, you can have more control over the JSON output and make it more consistent with your requirements.
Channels
A channel serves as a communication mechanism allowing one goroutine to send values of a specified type to another goroutine. It facilitates communication between goroutines, functioning similarly to pipes in other programming languages.
Creating a Channel:
To create a channel, you can use the make()
function and specify the type of the channel as its argument:
ch := make(chan int) // unbuffered channel
By default, the channel is unbuffered, meaning it can hold only one value at a time. If you attempt to send multiple values to an unbuffered channel, it will result in an error. However, you can create a buffered channel by specifying the buffer size as the second argument to the make()
function:
ch := make(chan int, 10) // buffered channel
Sending and Receiving:
You can use the <-
operator to send and receive values from a channel. Sending values to the channel is done as follows:
ch <- 10 // Sending 10 to the channel.
On the other hand, receiving values from the channel is done like this:
val := <-ch // Receiving from the channel.
To check if the channel is closed or not before receiving, you can use the following syntax:
val, ok := <-ch // Receiving from the channel and checking if the channel is closed.
Closing a Channel:
To close a channel, you can use the close()
function and pass the channel as an argument:
close(ch) // Closing the channel.
Send-Only and Receive-Only Channels:
You can create send-only and receive-only channels using the following syntax:
sendOnlyCh := make(chan<- int) // send-only channel
receiveOnlyCh := make(<-chan int) // receive-only channel
These special channels restrict the direction of data flow, allowing you to enforce certain communication patterns between goroutines.
Make sure to correctly utilize channels for effective communication and synchronization between goroutines in your Go programs.
Generics
Put simply, generics allow us to use variables to refer to specific types. This is an amazing feature because it allows us to write abstract functions that drastically reduce code duplication.
package Learnings
import "fmt"
func Generics() {
ans1, ans2 := splitSlice([]string{"Virat", "Kohli"})
fmt.Println(ans1)
fmt.Println(ans2)
}
func splitSlice[T any](s []T) ([]T, []T) {
mid := len(s) / 2
return s[:mid], s[mid:]
}
In the above example, T
is the name of the type parameter for the splitSlice
function, and we've specified that it must satisfy the any
constraint, which means it can be any
type. This makes sense because the body of the function doesn't care about the types of things stored in the slice. The function will work for slices of any type.
If you have read this far, then you are a great learner who is eager to acquire knowledge through any means. I appreciate you. Additionally, I have a tip for you: try building projects related to Go. This will enhance your knowledge and polish your skills.
I hope you learned something from this blog. If you have, don't forget to drop a like, follow me on Hashnode, and subscribe to my Hashnode newsletter so that you don't miss any future posts. You can also reach out to me on Twitter or LinkedIn. If you have any questions or feedback, feel free to leave a comment below. Thanks for reading and have a great day!