Mastering Go: Unleashing the Potential of Methods, Interfaces, Generics, and Concurrency | Part - 3
Tags: go, programming, beginners, microservices
Published: 2023-05-27 15:12:59

In the world of programming languages, Golang (also known as Go) has gained significant popularity for its simplicity, efficiency, and robustness. With its unique syntax and powerful features, Golang offers developers a versatile toolkit to build scalable and high-performance applications. In this blog, we will dive deep into some of the key aspects of Golang syntax, namely , Methods, Interfaces, Generics & Concurrency. Let's explore each topic in detail:

### Methods
- Go does not have classes. However, you can define methods on types.
- A method is a function with a special `receiver` argument.
= The receiver appears in its own argument list between the `func` keyword and the method name.
- In this example, the `Abs` method has a receiver of type `Vertex` named `v`.

```
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

// Remember: a method is just a function with a receiver argument.
// Here's Abs written as a regular function with no change in functionality.

func Abs_func(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	
	f := Vertex{12, 5}
	fmt.Println(Abs_func(f))
}
```
Output:

```
5
13
```

- You can only declare a method with a receiver whose type is defined in the same package as the method. You cannot declare a method with a receiver whose type is defined in another package (which includes the built-in types such as int).

```
package main

import (
	"fmt"
	"math"
)

type MyFloat float64

func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}

func main() {
	f := MyFloat(-math.Sqrt(2))
	fmt.Println(f)
	fmt.Println(f.Abs())
}
```
Output

```
-1.4142135623730951
1.4142135623730951
```
#### Pointer Receivers
- Go allows the declaration of methods with pointer receivers. Pointer receivers have the syntax *T, where T is a type (not a pointer type itself, such as *int). These methods can modify the value to which the receiver points. In contrast, methods with value receivers operate on a copy of the original value.
- For example, consider the Scale method defined on `*Vertex`. If the `*` is removed from the receiver declaration, the method will no longer be able to modify the original Vertex value. Pointer receivers are commonly used when methods need to modify their receiver.
- Removing the * from the receiver declaration changes the behavior to operate on a copy of the value instead.

```
package main 

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// Method With Pointer Receiver
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
	fmt.Println(v.X,v.Y)
}

// Scale Function
func (v Vertex) Scale_(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
	fmt.Println(v.X,v.Y)
}

func main() {
	v := Vertex{3, 4}
	v.Scale(10)
	fmt.Println(v.X, v.Y)
	fmt.Println(v.Abs(),"\n")
	
	f := Vertex{3, 4}
	f.Scale_(10)
	fmt.Println(f.X, f.Y)
	fmt.Println(f.Abs())
}
```
Output:

```
30 40
30 40
50 

30 40
3 4
5
```
#### Methods and Pointer Indirection
- you might notice that functions with a pointer argument must take a pointer:

```
var v Vertex
ScaleFunc(v, 5)  // Compile error!
ScaleFunc(&v, 5) // OK
```
- while methods with pointer receivers take either a value or a pointer as the receiver when they are called:

```
var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
```
Consider the following example:

```
package main

import "fmt"

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func ScaleFunc(v *Vertex, f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func main() {
	v := Vertex{3, 4}
	v.Scale(2)
	fmt.Println(v)
	ScaleFunc(&v, 10)
	fmt.Println(v,"\n")

	p := &Vertex{3, 4}
	p.Scale(2)
	fmt.Println(p)
	ScaleFunc(p,10)
	fmt.Println(p)
}
```
Output:

```
{6 8}
{60 80} 

&{6 8}
&{60 80}
```
- The equivalent thing happens in the reverse direction.
- Functions that take a value argument must take a value of that specific type:

```
var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // Compile error!
```
- while methods with value receivers take either a value or a pointer as the receiver when they are called:

```
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
```
- **In this case, the method call p.Abs() is interpreted as (*p).Abs().**

```
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func AbsFunc(v Vertex) float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := Vertex{3, 4}
	fmt.Println(v.Abs())
	fmt.Println(AbsFunc(v),"\n")

	p := &Vertex{3, 4}
	fmt.Println(p.Abs())
	fmt.Println(AbsFunc(*p))
}
```

Output:

```
5
5 

5
5
```

#### Choosing a value or pointer receiver

```
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
	v := &Vertex{3, 4}
	fmt.Printf("Before scaling: %+v, Abs: %v\n", v, v.Abs())
	v.Scale(5)
	fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}
```
Output:

```
Before scaling: &{X:3 Y:4}, Abs: 5
After scaling: &{X:15 Y:20}, Abs: 25
```


### Interfaces

In Go, interfaces provide a way to define sets of methods that a type must implement. This enables polymorphism and allows different types to be treated interchangeably if they satisfy the interface contract. Here's an example of using interfaces in Go:

```
package main

import (
	"fmt"
	"math"
)

// Shape is an interface for geometric shapes
type Shape interface {
	Area() float64
	Perimeter() float64
}

// Rectangle represents a rectangle shape
type Rectangle struct {
	Width  float64
	Height float64
}

// Area calculates the area of the rectangle
func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

// Perimeter calculates the perimeter of the rectangle
func (r Rectangle) Perimeter() float64 {
	return 2 * (r.Width + r.Height)
}

// Circle represents a circle shape
type Circle struct {
	Radius float64
}

// Area calculates the area of the circle
func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

// Perimeter calculates the circumference of the circle
func (c Circle) Perimeter() float64 {
	return 2 * math.Pi * c.Radius
}

func main() {
	rect := Rectangle{Width: 5, Height: 3}
	circle := Circle{Radius: 2.5}

	shapes := []Shape{rect, circle}

	for _, shape := range shapes {
		fmt.Printf("Area: %f\n", shape.Area())
		fmt.Printf("Perimeter: %f\n", shape.Perimeter())
		fmt.Println("------------------")
	}
}
```
Output:

```
Area: 15.000000
Perimeter: 16.000000
------------------
Area: 19.634954
Perimeter: 15.707963
------------------
```
#### Stringers

In Go, the `Stringer` interface is a built-in interface that allows types to define their own string representation. The `String()` method of the `Stringer` interface returns a string representation of the type. Here's an example of using the `Stringer` interface in Go:

```
package main

import "fmt"

type Person struct {
	Name string
	other string
}

func (p Person) String() string {
	return fmt.Sprintf("%v %v", p.Name, p.other)
}

func main() {
	a := Person{"Jay!", "Shiya Ram"}
	z := Person{"Jay Shree!", "Radhe Krishna"}
	fmt.Println(a, z)
}

```
Output:

```
Jay! Shiya Ram Jay Shree! Radhe Krishna
```

### Type Parameters
- Go functions can be written to work on multiple types using type parameters. The type parameters of a function appear between brackets, before the function's arguments.

```
func Index[T comparable](s []T, x T) int
```
- This declaration means that `s` is a slice of any type `T` that fulfills the built-in constraint `comparable`. `x` is also a value of the same type.
- `comparable` is a useful constraint that makes it possible to use the `==` and `!=` operators on values of the type. In this example, we use it to compare a value to all slice elements until a match is found. This `Index` function works for any type that supports comparison.

```
package main

import "fmt"

// Index returns the index of x in s, or -1 if not found.
func Index[T comparable](s []T, x T) int {
	for i, v := range s {
		// v and x are type T, which has the comparable
		// constraint, so we can use == here.
		if v == x {
			return i
		}
	}
	return -1
}

func main() {
	// Index works on a slice of ints
	si := []int{10, 20, 15, -10}
	fmt.Println(Index(si, 15))

	// Index also works on a slice of strings
	ss := []string{"foo", "bar", "baz"}
	fmt.Println(Index(ss, "hello"))
}
```
Output:
```
2
-1
```

### Goroutines
- A goroutine is a lightweight thread managed by the Go runtime.

```
go f(x, y, z)
```
- starts a new goroutine running

```
f(x, y, z)
```
- The evaluation of f, x, y, and z happens in the current goroutine and the execution of f happens in the new goroutine.
- Goroutines run in the same address space, so access to shared memory must be synchronized. The sync package provides useful primitives, although you won't need them much in Go as there are other primitives.

```
package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}
```
Output:

```
hello
world
world
hello
world
hello
hello
world
world
hello
```
- In this code, we have two functions: `say` and `main`. The `say` function takes a string parameter `s` and prints it five times with a delay of 100 milliseconds between each print.
- In the `main` function, we launch a new goroutine by calling `go say("world")`. This means that the `say` function with the argument "world" will be executed concurrently with the main goroutine.
- Simultaneously, the main goroutine continues executing and calls `say("hello")`. As a result, "hello" will be printed five times in the main goroutine.
- The output of this program will be somewhat unpredictable due to the concurrent nature of goroutines. It may vary on each execution, but you can expect to see interleaved "hello" and "world" messages.

### Channels

channels provide a way for goroutines to communicate and synchronize their execution. Channels are used to pass data between goroutines and ensure safe concurrent access to shared resources. Here's an explanation of channels in Go:

**1. Channel Creation:**
 - To create a channel, you use the make function with the chan keyword followed by the type of data the channel will transmit. For example:

```
ch := make(chan int) // Creates an unbuffered channel of type int
```

**2. Channel Operations:**

 Channels support two fundamental operations: sending and receiving data.
 - Sending Data: To send data through a channel, you use the `<-` operator in the form `channel <- value`. For example:

```
ch <- 177 // Sends the value 177 into the channel 
```
 - Receiving Data: To receive data from a channel, you use the `<-` operator on the left-hand side of an assignment. For example:

```
value := <-ch // Receives a value from the channel and assigns it to the variable "value"
```

Consider the following example:

```
package main

import "fmt"

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // send sum to c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // receive from c

	fmt.Println(x, y, x+y)
}
```
Output:

```
-5 17 12
```

The example code sums the numbers in a slice, distributing the work between two goroutines. Once both goroutines have completed their computation, it calculates the final result.

#### Buffered Channels

- Channels can be buffered. Provide the buffer length as the second argument to make to initialize a buffered channel:

```
ch := make(chan int, 100)
```

- Sends to a buffered channel block only when the buffer is full. Receives block when the buffer is empty.

```
package main

import (
	"fmt"
)

func main() {
	// Create a buffered channel with a capacity of 3
	ch := make(chan int, 3)

	// Send values to the channel
	ch <- 1
	ch <- 2
	ch <- 3

	// Attempting to send another value to the channel would block since the buffer is full

	// Receive values from the channel
	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)

	// Attempting to receive another value from the channel would block since the buffer is empty
}
```
Output:

```
1
2
3
```
- In this example, we create a buffered channel `ch` with a capacity of 3 by specifying the capacity as the second argument to the `make` function.
- We then send three values `(1, 2, and 3)` to the channel using the `<-` operator. Since the channel has a buffer capacity of 3, these sends will not block.
- After sending the values, we receive and print them using the `<-` operator and `fmt.Println()` statements. Again, since the channel is buffered and contains three values, these receives will not block.
- However, if we attempt to send or receive more values to/from the channel, it would block. For example, trying to send a value when the buffer is full or receive a value when the buffer is empty would cause the corresponding goroutine to block until space becomes available or a value is sent.
-Buffered channels are useful when you want to decouple the send and receive operations in terms of timing, allowing the sender and receiver to operate independently up to the buffer capacity.

**To continue reading and explore the other chapter, simply follow this link:** [Link To Other Chapters](https://dev.to/sahil_4555/mastering-go-a-comprehensive-guide-to-golang-syntax-part-2-22km)