In Go, statements are used to instruct the program to perform a specific task or action. They are written as a sequence of tokens and are executed by the Go interpreter or compiler.
Statements in Go can be classified into various categories such as,
Understanding the different types of statements and their usage is essential for effective Go programming. By using the appropriate statements for each task, programmers can write more efficient and effective code.
Today, we will see some rarely known but useful statements in Golang with real examples.
Note: I had skipped some basic statements like declaration statements(var, const, type), control flow statements(if…else, for, switch), as they are simple and easy to understand.
Let’s explore Golang's statements one by one.
Positive thinking leads to positive outcomes. Try out Justly, build good habits, and start thinking positively today!
The panic statement is used to stop the normal execution of a program with error
.
The recover function is used to handle the panic
and continues the execution
. It returns the value that was passed to the panic
function, which can be used to return an appropriate error response to the client.
For instance, we are building a web server in Go, and we want to ensure that the server does not crash in case of an unexpected error. We can use panic
and recover
to recover from such errors and return a meaningful error response to the client.
Example:
func handleRequest(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
log.Println("Recovered error: ", r)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
// code that may cause a panic, will be handled by recover()
panic('Request not handled properly')
}
// output will be printed
Recovered error: Request not handled properly
By using panic
and recover
in this way, we can ensure that our web server remains stable and responds gracefully to unexpected errors, rather than crashing or returning meaningless error messages to the client.
This allows you to jump to a labeled statement within the same function.
Here’s an example of how you can use the goto
statement:
func main() {
i := 0
loop:
if i < 10 {
fmt.Printf("%d ", i)
i++
goto loop
}
}
//output
0 1 2 3 4 5 6 7 8 9
While the goto statement can be useful in certain situations, it is generally not recommended, because it can make the code harder to understand and maintain. It can also lead to complex and convoluted control flow, which can make debugging more difficult.
It’s worth noting that the use of goto
is limited to the same function and can only be used to jump to a labeled statement within that function. We can’t use a goto
statement to jump to a statement in a different function.
These statements are used to alter the normal flow of execution in loops.
The break is used to exit a loop prematurely inside a for
, switch
, or select
statement. It immediately terminates the innermost loop and control resumes at the next statement following the loop.
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Printf("%d ", i)
}
//output
0 1 2 3 4
continue is used to skip the current iteration of a loop and move on to the next iteration. When continue
is encountered, the current iteration is immediately terminated and the loop continues with the next iteration.
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Printf("%d ", i)
}
//output
1 3 5 7 9
An anonymous function is a function without a name. It can be defined inline and used immediately, or assigned to a variable and used later.
// execute when used
sum := func(a, b int) int {
return a + b
}
// will execute immediately
func() {
fmt.Println("Hello, anonymous function!")
}()
fmt.Println("Sum is : ", sum(3, 4))
// output
Hello, anonymous function!
Sum is : 7
A function closure is created when a function returns an inner function that references a variable defined in the outer function’s scope.
Let’s consider an example where we have a function that returns another function that adds a value to a running total:
func adder() func(int) int {
total := 0 // outer function's variable
return func(x int) int { // can modify total's value across function calls
total += x // references total and modify it
return total
}
}
// use this function as below
a := adder()
fmt.Println(a(1)) // 0 + 1 = 1
fmt.Println(a(2)) // 1 + 2 = 3
fmt.Println(a(3)) // 3 + 3 = 6
Function closures can be used for a wide range of applications, from managing state in concurrent code to creating memoized functions.
We all are using _
(underscore), when we want to ignore the returned values of functions. This is known as a blank identifier in Go.
_, err := someFunction()
if err != nil {
fmt.Println("Error:", err)
}
Variadic functions are functions that can take a dynamic number of arguments. This is useful when you don’t know how many arguments you will need to pass to a function. It will take ...
as an argument.
Here is an example of concatenating an arbitrary number of strings using a variadic function.
func concatStrings(separator string, words ...string) string {
var result string
for i, s := range words {
if i > 0 {
result += separator
}
result += s
}
return result
}
// Here's an example of how we can use this function in a Go program:
result := concatStrings(" ", "hello", "world", "!")
fmt.Println(result) // Output: "hello world !"
By using a variadic function like this, we can provide a flexible and convenient way for users without having to worry about the details of how the function works.
Type assertions are used to convert an interface value to a concrete type. This is useful when you have an interface value and you want to use it as a specific type.
Suppose we are working on a command-line tool that takes user input and performs some operation based on that input. We want to be able to handle a wide range of input types, including integers, floats, and strings. To do this, we can use a variadic function that accepts a slice of interface{}
types, which can hold any value.
Here’s an example of this:
func performOperation(args ...interface{}) {
for _, arg := range args {
switch val := arg.(type) {
case int:
// Handle integer input
fmt.Println("Integer input:", val)
case float64:
// Handle floating-point input
fmt.Println("Float input:", val)
case string:
// Handle string input
fmt.Println("String input:", val)
default:
// Handle unrecognized input
fmt.Println("Unrecognized input:", val)
}
}
}
// use function as below
performOperation(77, "hey", 0.54)
By using type assertions in this way, we can handle a wide range of input types in a flexible and extensible way, without having to define separate functions or code paths for each type.
This can be used to defer the execution of a function until the surrounding function has been completed.
Here’s an example of using the defer
statement to ensure that a file is closed after it's opened,
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // defer the file.Close() call until the function returns
// read file contents
return nil
}
By deferring the file.Close()
call until after the function returns, we ensure that the file is always properly closed, even in the case of an error. This can help avoid resource leaks and other problems associated with improperly closing files.
This statement is commonly used in Go for tasks like closing files, releasing locks, and releasing other resources like database instances that need to be cleaned up after a function has been completed.
This can be used to define a label for a certain block of code. Labels are most commonly used with the break,continue and goto
statements to control the flow of execution in nested loops or switch statements.
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i*j >= 10 {
break outer
}
fmt.Println(i, j)
}
}
If we had used a break
statement without the label, it would have only broken out of the inner loop, and the outer loop would have continued to execute.
It’s worth noting that labels can only be used within the same function as the labeled statement. You cannot use a label to jump to a statement in a different function.
This is used to wait on multiple channels for input. It blocks until one of the channels has data ready to be processed.
This can be used in many different situations, such as waiting for data from multiple sources or coordinating multiple goroutines in concurrent programming.
select {
case <- channel1:
// handle channel1 case
case data := <- channel2:
// handle channel2 case
case channel3 <- value:
// handle channel3 case
default:
// handle default case
}
In this example, we have a select
statement that waits for data to be sent or received on one of three channels. The default
case is used to handle situations where none of the other cases are ready. It blocks until one of the communication operations is ready. This means that the statement will not exit until one of the cases is executed.
We have explored several rarely known statements that can be incredibly useful in certain situations with real-world examples.
As a Golang developer, these statements can greatly enhance your programming skills and make you a more efficient developer.
We’re Grateful to have you with us on this journey!
Suggestions and feedback are more than welcome!
Please reach us at Canopas Twitter handle @canopas_eng with your content or feedback. Your input enriches our content and fuels our motivation to create more valuable and informative articles for you.
That’s it for today. Always aim for the top 😎.