How to GoLang

An AMAZING language created by Google in collaboration with Rob Pike, Ken Thomson, and Robert Griesemer.

Advantages:

  • Fast, compiles directly into machine code without using an interpreter.
  • Easy to learn, very good documentation, and many things are simplified.
  • Scales very well, supports concurrent programming through "GoRoutines".
  • Automatic garbage collector, automatic memory management.
  • Included formatting engine, no need for third parties.
  • No libraries required for testing or benchmarks because they are already included.
  • Very little boilerplate for creating applications.
  • Has an API for network programming, included as a standard library.
  • VERY fast, in some benchmarks it is faster than backend applications made in Java and Rust.
  • Built-in template system, GREAT for working with HTMX.
  • ui (frontend-related content in case of server-side rendering)
    • html (templates)
    • static (multimedia and style static content)
      • assets
      • css
  • internal (content related to tools and reusable entities throughout the project)
    • models
    • utils
  • cmd
    • web (contains the application logic)
      • domain (business logic)
      • routes (available routes)

How Does GoLang Work?

GoLang uses a base file called go.mod, which will contain the main module that will be called the same as the project, and also the version of Go used. Then each file will have the extension ".go" to identify that it is a package belonging to the language.

But... what is a package? If you come from JavaScript you can think of it in the same way as an ES module since it is used to encapsulate related logic. But unlike ES modules, the package is identified by the lines of code "package packageName" in camel case the name of the package in question and it imports the location of the package. Different files containing logic belonging to the same package can be arranged separately BUT they must be under the same parent folder as this is of utmost importance to later import said package in different ones.

To import a different package is done through the word "import" plus the path to which the package belongs.

import "miProject/cmd/web/routes"

If you need more than one package at a time, it is not necessary to repeat the line of code since it can be grouped using "()" the various packages:

import (
    "miProject/cmd/web/routes"
    "miProject/internal/models"
)

It is worth mentioning that then Go will relate the final name of the path with the use of the package so in order to use logic contained in it will be done thinking of it as if it were an object, where each property represents a logical element of the package:

routes.MyRoute

Private and public package methods:

If the method starts with lowercase it is a private method, it cannot be accessed from outside the package itself.

func myFunction()

If the method starts with uppercase it is a public method, it can be accessed by importing the package from another.

func MyFunction()

Package scope

Let's see a Go file

package main - package name

import "fmt" - fmt package is imported, no path because it's Go's own

var number int = 2

func main() {
    i, j := 42, 2701 - local variables to the method, i with value 42 and j with value 2701

    fmt.Println(i) - using the "Println" method of the "fmt" package
}

You surely have noticed something, "number" has a type "int" preceding the value assignment, while "i" and "j" do not, this is because like Typescript, Go infers the type for those primitives. Let's see how to work with types.

Data Types

  • bool = true / false
  • string = string of characters
  • int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, uintptr = integer numeric values with their limits, these are generally 32 bits on 32-bit systems and 64 for 64-bit systems. Integer should be used unless there is a specific reason to use a restricted value.
  • byte === uint8
  • rune === int32
  • float32, float64 = represents real numeric values
  • complex64, complex128 = complex numbers that have a real part and an imaginary part.

Structs

Represents a collection of properties, you can think of it as a Typescript interface, as it represents the contract that must be followed when creating a property. Important, if you want that property to be accessible outside the package, remember it should start with "uppercase".

type Person struct {
    Name string
    LastName string
    Age int
}

var person = Person {
    Name: "Gentleman",
    LastName: "Programming",
    Age: 31
}

fmt.Println(person.Name)

Another way:

var persona2 = Persona{"Gentleman", "Programming", 31}

Arrays

Now the fun begins, arrays are quite different from what we are used to as they MUST have the maximum number of elements they are going to contain inside:

var a [10]int - creates an array of 10 elements of type int

a[0] = "Gentleman"

Or also

var a = [2]int{2, 3}

fmt.Println(a) - [2 3]

If we need it to be dynamic we can talk about "slices". A "slice" is a portion of an existing array or a representation of a collection of elements of a certain type.

var primes = [6]int{2 ,3 ,5, 7, 11, 13}
var s []int = primes[1:4] - creates a slice using "primes" as a base from position 1 to 4

fmt.Println(s) - [3 5 7] 

s = append(s , 14)

fmt.Println(s) - [3 5 7 14] 

fmt.Println(primes) - [2 3 5 7 14 13]

You can also omit values ​​for maximum and minimum ranges making them have default values:

var a [10]int

is the same as

a[0:10]
a[:10]
a[0:]
a[:]

Make Method

To create dynamic slices you can use the included "make" method, this will create an array filled with empty elements and return a slice referring to it. The "len" method can be used to see how many elements it currently contains and "cap" to see its capacity, that is, how many elements it can hold.

a := make([]int, 0, 5)  // len(a)=0 cap(a)=5

Pointers

If you come from Javascript...

this will take a bit, but let's see together the following example:

type ElementType struct {
    name string 
}

var exampleElement = ElementType {
    name: "Gentleman",
} 

func MyFunction(element ElementType) {
    ...
}

MyFunction(exampleElement)

Here you might think that we are working on the element "exampleElement", but it's quite the opposite. By default, Go will create a copy of the element itself to work with it, so the usage of "element" inside the function "MyFunction" is different from "exampleElement"... it's a copy.

So if we want to work with the same element passed as a parameter to the function, a pointer must be used. Normally when we create a variable, we mistakenly say that we create a property that holds a value inside it, when in reality what we are doing is creating a memory space, containing a value inside it, and then creating a reference (pointer) to that memory space which is represented by the name we give to our property:

var a = 1

It creates a memory space which inside it contains the value "1" and we create a reference to that memory space called "a". The difference with Javascript is that this reference is not passed to the method unless we have created a pointer to it!

var p *int // pointer "p" that will reference a property of type "int"

i := 42
p = &i // create a direct pointer to the property "i"

// If we want to access the value referenced by the pointer "p", we use the pointer's name preceded by the "*" symbol
fmt.Println(*p) // 42

*p = 21

fmt.Println(*p) // 21

Where this changes is if we point to a "struct", as it would be a bit cumbersome to do (*p).Property, it reduces to using it as if it were the struct itself:

v := Person{"Gentleman"}
p := &v
p.Name = "Programming"
fmt.Println(v) // {Programming}

Default Values

In Go, when you declare a variable without explicitly assigning a value, it takes on a default value based on its type. Here's a table summarizing the defaults:

Default Values for Data Types:

  • bool: false
  • string: "" (empty string)
  • Numeric Types: 0
  • array: [0...] (zero-valued)
  • map: nil (uninitialized)
  • slice: nil (uninitialized)
  • pointer: nil (uninitialized)
  • function: nil (uninitialized)

Range Loop

The range loop is a powerful construct for iterating over sequences like slices, arrays, maps, and strings. It provides two components: the index (i) and the value (v) of each element. Here are three common variations:

  • Full Iteration:
var arr = []int{5, 4, 3, 2, 1}

for i, v := range arr {
  fmt.Printf("index: %d, value: %d\n", i, v)
}

This approach iterates over both the index and value of each element in arr.

  • Ignoring Index:
for _, v := range arr {
  fmt.Printf("value: %d\n", v)
}

The underscore (_) discards the index information, focusing only on the element values.

  • Ignoring Value:
for i, _ := range arr {
  fmt.Printf("index: %d\n", i)
}

Similarly, you can use an underscore to skip the value and access only the indices.

Maps

Maps are unordered collections that associate unique keys (of any hashable type) with values. Go provides two ways to create and work with maps:

  1. Using make Function:

    type Persona struct {
      DNI, Nombre string
    }
    
    var m map[string]Persona
    
    func main() {
      m = make(map[string]Persona)
      m["123"] = Persona{"123", "pepe"}
      fmt.Println(m["123"])
    }
    
  2. Map Literal:

    type Persona struct {
      DNI, Nombre string
    }
    
    var m = map[string]Persona{
      "123": Persona{"123", "pepe"},
      "124": Persona{"124", "jorge"},
    }
    
    func main() {
      fmt.Println(m)
    }
    

    Map literals offer a concise way to initialize maps with key-value pairs.

Mutating Maps

  • Insertion:

    m[key] = element
    

    Adds a new key-value pair to the map m.

  • Retrieval:

    element = m[key
    

Functions

Functions are reusable blocks of code that perform specific tasks. They are declared with the func keyword, followed by the function name, parameter list (if any), return type (if any), and the function body enclosed in curly braces.

Here's an example:

func greet(name string) string {
  return "Hello, " + name + "!"
}

func main() {
  message := greet("Golang")
  fmt.Println(message)
}

Function Values

Functions can be assigned to variables, allowing you to pass them around like any other value. This enables powerful techniques like higher-order functions.

Here's an example demonstrating how to pass a function as an argument and call it indirectly:

func CallCallback(callBack func(float64, float64) float64) float64 {
  return callBack(3, 4)
}

func hypot(x, y float64) float64 {
  return math.Sqrt(x*x + y*y)
}

func main() {
  fmt.Println(hypot(5, 12))
  fmt.Println(CallCallback(hypot))
}

Closures

Closures are a special type of function that captures variables from its enclosing environment. This allows the closure to access and manipulate these variables even after the enclosing function has returned.

Here's an example of a closure that creates an "adder" function with a persistent sum:

func adder() func(int) int {
  sum := 0

  return func(x int) int {
    sum += x
    return sum
  }
}

func main() {
  pos, neg := adder(), adder()

  for i := 0; i < 10; i++ {
    fmt.Println(
      pos(i),
      neg(-2*i),
    )
  }
}

Methods

Go doesn't have classes, but it allows defining methods on types (structs, interfaces). A method is a function associated with a type, taking a receiver argument (usually the type itself) that implicitly refers to the object the method is called on.

Here's an example of a Persona struct with a Saludar method:

type Persona struct {
  Nombre, Apellido string
}

func (p Persona) Saludar() string {
  return "Hola " + p.Nombre
}

func main() {
  p := Persona{"Pepe", "Perez"}
  fmt.Println(p.Saludar())
}

Methods can also be defined on non-struct types:

type Nombre string

func (n Nombre) Saludar() string {
  return "Hola " + string(n)
}

func main() {
  nombre := Nombre("Pepe")
  fmt.Println(nombre.Saludar())
}

Methods can accept pointers as receivers, enabling modifications to the original object:

type Persona struct {
  nombre, apellido string
}

func (p *Persona) cambiarNombre(n string) {
  p.nombre = n
}

func main() {
  p := Persona{"pepe", "perez"}
  p.cambiarNombre("juan")
  fmt.Println(p) // Output: {juan perez}

  pp := &Persona{"puntero", "persona"}
  pp.cambiarNombre("punteroNuevoNombre")
  fmt.Println(*pp) // Output: {punteroNuevoNombre persona}
}

Go automatically dereferences pointer receivers when necessary, so you don't always need to use the explicit * operator.

Interfaces

Interfaces define a set of methods that a type must implement. They provide a way to achieve polymorphism, allowing different types to be used interchangeably as long as they implement the required methods.

Here's an example of an Interface that defines two methods, Saludar and Moverse:

type Persona interface {
  Saludar() string
  Moverse() string
}

type Alumno struct {
  Nombre string
}

func (a Alumno) Saludar() string {
  return "Hola " + a.Nombre
}

func (a Alumno) Moverse() string {
  return "Estoy caminando"
}

func main() {
  var persona Persona = Alumno{
    "Pepe",
  }

  fmt.Println(persona.Saludar())
  fmt.Println(persona.Moverse())
}

Interface Values with Nil

Interface values can be nil, indicating that they don't hold a reference to any specific object. Here's an example demonstrating how to handle nil interface values:

type I interface {
  M()
}

type T struct {
  S string
}

func (t *T) M() {
  if t == nil {
    fmt.Println("<nil>")
    return
  }
  fmt.Println(t.S)
}

func main() {
  var i I

  var t *T
  i = t
  describe(i)
  i.M() // Output: <nil>

  i = &T{"hello"}
  describe(i)
  i.M() // Output: hello
}

func describe(i I) {
  fmt.Printf("(%v, %T)\n", i, i)
}

Empty Interfaces

If you don't know the specific methods an interface might require beforehand, you can create an empty interface using the interface{} type. This allows you to store any value in the interface, but you won't be able to call methods on it directly.

var i interface{}

Type Assertion

When we use an empty interface go interface{}, we may use any kind of type BUT, this also comes with problems. How do we know if the parameter of a method is of the expected type if if it's an empty interface ? Here's where Type Assertions come handy, as they provide the possiblity of testing if the empty interface is of the expected type.

t := i.(T)

This means that the interface value i holds the concrete type T and assigns the underlying T value to the variable t.

If i does not hold a T, this will trigger a panic.

You can test if the interfaces faclue holds a specific type by using a second parameter, just like we do with err:

t, ok := i.(T)

This will save true or false inside ok. If false, t will save a zero value inside and no panic will occur.

func main() {
    var i interface{} = "hello"

    s := i.(string)
    fmt.Println(s) // hello

    s, ok := i.(string)
    fmt.Println(s, ok) // hello true

    f, ok := i.(float64)
    fmt.Println(f, ok) // 0 false

    f = i.(float64) // panic: interface conversion: interface {} is string, not float64
    fmt.Println(f) // nothing, it will panic before
  }

Type Switches

It provides the possiblity of doing more than one Type Assertion in series.

Just like a regular switch statemenet, but we use types instead of values, and the later ones will be compared against the type of the value held by the given interface value.

switch v := i.(type) {
    case T: 
    // if v has type T
    case S:
    // if v has type S
    default:
    // if v has neither type T or S, it will have the same type as "i"
  }

Acclaration: just like Type Assertions, we use a type as a parameter go i.(T), but instead of using T, we need to use the keyword type.

This is great when executing different logics which depends on the type of the parameter:

type Greeter interface {
    SayHello()
}

type Person struct {
    Name string
}

func (p Person) SayHello() {
    fmt.Printf("Hello, my name is %s!\n", p.Name)
}

type Number int

func (n Number) SayHello() {
    if n%2 == 0 {
        fmt.Printf("Hello, I'm an even number: %d!\n", n)
    } else {
        fmt.Printf("Hello, I'm an odd number: %d!\n", n)
    }
}

func main() {
    greeters := []Greeter{
        Person{"Alice"},
        Person{"Bob"},
        Number(3),
        Number(4),
    }

    for _, greeter := range greeters {
        switch value := greeter.(type) {
        case Person:
            value.SayHello()
        case Number:
            value.SayHello()
        }
    }
}

Stringers

It's a type that defines itself as a string, it's defined by the fmt package and it's used to print values.

type Person struct {
  Name string
  Age int
}

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

func main() {
    a := Person{"Gentleman Programming", 32}
    z := Person{"Alan Buscaglia", 32}

    fmt.Println(a, z) // Gentleman Programming (32 years) Alan Buscaglia (32 years)
  }

Example using Stringers to modify the way we show an IpAdress when using fmt.Println :

type IPAddr [4]byte

func (ip IPAddr) String() string {
  str := ""
  
	for i, ipValue := range ip {
    str += fmt.Sprint(ipValue)

		if i < len(ip)-1 {
			str += "."
		}	
  }
	
	return fmt.Sprintf("%s", str)
}

// TODO: Add a "String() string" method to IPAddr.

func main() {
	hosts := map[string]IPAddr{
		"loopback":  {127, 0, 0, 1},
		"googleDNS": {8, 8, 8, 8},
	}

	for _, ip := range hosts {
		fmt.Println(ip)
	}
}

Errors

To show erros, Go uses error values to express error states, and for this, the error type exists and it's similar to the fmt.Stringer interface:

type error interface {
  Error() string
}

Exactly as with fmt.Stringer the fmt package looks for the error interface when printing values. Normally methods return an error value and we should use it to manage what to do in case it's different to nil:

i, err := strconv.Atoi("42")

if err != nil {
  fmt.Println("couldn't convert number: %v\n", err)
  return
}

fmt.Println("Converted integer: ", i)

Readers

Another great interface which represents the read end of a stream of data, this data may be streamed over files, network connections, compressors, ciphers, etc.

And it has a Read method:

func (T) Read(b []byte) (n int, err error)

This method will populate the byte array with data and returns the number of bytes populated and an error value. It returns an io.EOF error when the stream ends.

func main() {
  data := "Gentleman Programming"


  // create a new io.Reader reading from data
  reader := strings.NewReader(data)

  // create a buffer to store the copied data
  var buffer strings.Builder

  // copy data from the reader to a buffer. io.Copy reads from the reader and writes to the writer until either EOF is reached on the reader or an error occurs
  n, err := io.Copy(&buffer, reader)
 
  if err != nil {
    fmt.Println("Error:", err)
  } else {
      fmt.Println("\n%d bytes copied successfully. \n", n)

      // access the data copied into the buffer
      fmt.Println("Copied Data:", buffer.String())
    }
}

Example, let's get a ciphered string and decode it !

package main

import (
	"io"
	"os"
	"strings"
)

type rot13Reader struct {
	r io.Reader
}

func (rr *rot13Reader) Read(p []byte) (n int, err error) {
	n, err = rr.r.Read(p)
	for i := 0; i < n; i++ {
		if (p[i] >= 'A' && p[i] <= 'Z') || (p[i] >= 'a' && p[i] <= 'z') {
			if p[i] <= 'Z' {
        // p[i] - 'A' calculates the position of the current character relative to 'A', then we add 13 as the ROT13 algorithm shifts each letter in the alphabet by 13 positions, 
        // then we apply '%26' to ensure that the result is within the range of the alphabet (26 letters) 
        // and at the end we add 'A' wich converts the result back to the ASCII value of a letter
				p[i] = (p[i]-'A'+13)%26 + 'A'
			} else {
				p[i] = (p[i]-'a'+13)%26 + 'a'
			}
		}
	}
	return
}

func main() {
	s := strings.NewReader("Lbh penpxrq gur pbqr!")
	r := rot13Reader{s}
	io.Copy(os.Stdout, &r)
}

Images

The package iamges defines the Image interface, which is a powerful tool to work with images as you can create, manipulate, and decode various types of images such as PNG, JPEG, GIF, BMP, and more:

package image

type Image interface {
  ColorModel() color.Model
  Bounds() Rectangle
  At(x, y int) color.Color
}

Let's create a black small image with a red pixel in the center:

package main

import (
	"image"
	"image/color"
	"image/png"
	"os"
)

func main() {
	// Create a new RGBA image with dimensions 100x100
	img := image.NewRGBA(image.Rectangle(0, 0, 100, 100))

	// Set all pixels to black
	for x := 0; x < 100; x++ {
		for y := 0; y < 100; y++ {
			img.Set(x, y, color.Black)
		}
	}

	// Set the pixel at the center to red
	img.Set(50, 50, color.RGBA{255, 0, 0, 255})

	// Create a PNG file to save the image
	file, err := os.Create("simple_image.png")
	if err != nil {
		panic(err)
	}
	defer file.Close()

	// Encode the image to PNG format and save it to the file
	err = png.Encode(file, img)
	if err != nil {
		panic(err)
	}

	println("Simple image generated successfully!")
}

GoRoutines

As we mentioned before, Go is a language that supports concurrent programming through "GoRoutines". A GoRoutine is a lightweight thread managed by the Go runtime, allowing you to run multiple functions concurrently.

BUT it's different than other languages, as it's a virtual thread that runs on a real thread, and it's managed by the Go runtime.

To execute a function as a GoRoutine, you just need to add the go keyword before the function call:

	go f(x, y, z)

This will run the function f(x, y, z) concurrently in a new GoRoutine. The parameters are evaluated at the time of the function call, so if they change later, the GoRoutine will use the updated values.

Let's see an example:

package main

import (
  "fmt"
)

func say(s string) {
  for i := 0; i < 3; i++ {
    fmt.Println(s)
  }
}

func main() {
  // Launch a new goroutine to run the say function with "Hello"
  go say("Hello")

  // Print "World" 3 times in the main function
  for i := 0; i < 3; i++ {
    fmt.Println("Gentleman")
  }
}

When you run this code, the output won't necessarily be "Hello" followed by "Gentleman" three times each. This is because the goroutines are running concurrently. You might see "Hello" and "Gentleman" mixed together.

Goroutines are lightweight, so you can create thousands of them without any performance issues. They are managed by the Go runtime, which schedules them efficiently on real OS threads.

Another great feature is that the run in the same address space, so they can communicate with each other using channels sharing memory, but, this also needs to be managed and synchronized.

To do so we can use channels:

Channels

They will be our way to communicate between goroutines, they are typed and can be used to send and receive data with the channel operator <-:

ch <- v // Send v to channel ch.
v := <-ch // Receive from ch, and assign value to v.

The data will flow in the direction of the arrow, so if you want to send data to a channel, you should use the arrow pointing to the channel, and if you want to receive data from a channel, you should use the arrow pointing from the channel.

You can also create a channel with the use of the make function:

ch := make(chan int)

This will create a channel that will send and receive integers.

By default, sends and receives block until the other side is ready. This allows goroutines to synchronize without we having to manually manage that synchronization.

package main

import (
  "fmt"
)

func say(s string, ch chan string) {
  for i := 0; i < 3; i++ {
    fmt.Println(s)
    ch <- s // Send "Hello" to the channel after each print
  }
}

func main() {
  // Create a channel to hold strings
  ch := make(chan string)

  // Launch a new goroutine to run the say function
  go say("Hello", ch)  

  // Wait infinitely for messages on the channel (ensure all "Hello" are printed)
  for {
    msg := <-ch // Receive message from the channel
    fmt.Println("Received:", msg)
  }

  fmt.Println("Gentleman") // Print "Gentleman" after receiving all messages
}

Buffered Channels

All channels can be buffered, this means that they can hold a limited number of values without a corresponding receiver for those values.

When the channel is full, the sender will block until the receiver has received a value. This is extremely useful when you want to send multiple values and you don't want to loose them if the receiver is not ready.

ch := make(chan int, 100)

This will create a channel that can hold up to 100 integers.

If you send more than 100 values to the channel, the sender will block until the receiver has received some values.

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	ch <- 3 // fatal error: all goroutines are asleep - deadlock!

	fmt.Println(<-ch)
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

Range and Close

You can close a channel at any time ! a recommended time to close a channel is when you want to signal that no more values will be sent on it and the one to do it should be the sender, never the receiver as sending on a closed channel will cause a panic:

v, ok := <-ch
ok will be false if there are no more values to receive and the channel is closed.

To receive values from a chanel until it's closed you can use range:

for i:= range ch {
	fmt.Println(i)
}

Do we need to close them ? Not necessarily, only if the receiver needs to know that no more values will be sent, or if the sender needs to tell the receiver that it's done sending values, this way we will terminate the range loop.

Example:

func say(s string, ch chan string) {
  for i := 0; i < 3; i++ {
    fmt.Println(s)
    ch <- s // Send "Hello" to the channel after each print
  }
  close(ch) // Close the channel after sending messages
}

func main() {
  // Create a channel to hold strings
  ch := make(chan string)

  // Launch a new goroutine to run the say function
  go say("Hello", ch)  

  // Loop to receive and print messages until channel is closed
  for {
    msg, ok := <-ch // Receive message and check channel open state
    if !ok {
      break // Exit loop if channel is closed
    }
    fmt.Println("Received:", msg)
  }

  fmt.Println("Messages received. Exiting.")
}

GoRoutines Select

The select statement lets a goroutine wait on multiple communication operations. It blocks until one of its cases can run, then it executes that case.

It's useful when you want to wait on multiple channels and perform different actions based on which channel is ready.

func say(s string, ch chan string) {
  for i := 0; i < 3; i++ {
    fmt.Println(s)
    ch <- s // Send "Hello" to the channel after each print
  }
  // Close the channel after the loop finishes sending messages
  close(ch)
}

func main() {
  // Create a channel to hold strings
  ch := make(chan string)

  // Launch a new goroutine to run the say function
  go say("Hello", ch)

  // Use select to handle messages from the channel or a timeout
  for {
    select {
    case msg, ok := <-ch: // Receive message and check channel open state
      if !ok {
        fmt.Println("Channel closed. Exiting.")
        break
      }
      fmt.Println("Received:", msg)
    case <-time.After(1 * time.Second): // Timeout after 1 second if no message received
      fmt.Println("Timeout waiting for message.")
      break
    }
  }
}

You can also use a default case in a select statement, this will run if no other case is ready:

func say(s string, ch chan string) {
  for i := 0; i < 3; i++ {
    fmt.Println(s)
    ch <- s // Send "Hello" to the channel after each print
  }
  close(ch) // Close the channel after sending messages
}

func main() {
  // Create a channel to hold strings
  ch := make(chan string)

  // Launch a new goroutine to run the say function
  go say("Hello", ch)  

  // Use select with default case
  for {
    select {
    case msg, ok := <-ch:
      if !ok {
        fmt.Println("Channel closed. Exiting.")
        break
      }
      fmt.Println("Received:", msg)
    case <-time.After(1 * time.Second):
      fmt.Println("Timeout waiting for message.")
      break
    default:
      fmt.Println("Nothing to receive or timeout yet.")
    }
  }
}

Now let's do an exercise where we will check if two node trees have the same sequence of values:

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

func isSameSequence(root1, root2 *TreeNode) bool {
	seq1 := make(map[int]bool)
	seq2 := make(map[int]bool)

	traverse(root1, seq1)
	traverse(root2, seq2)

	return equal(seq1, seq2)
}

func traverse(node *TreeNode, seq map[int]bool) {
	if node == nil {
		return
	}

	seq[node.Val] = true
	traverse(node.Left, seq)
	traverse(node.Right, seq)
}

func equal(seq1, seq2 map[int]bool) bool {
	if len(seq1) != len(seq2) {
		return false
	}

	for val := range seq1 {
		if !seq2[val] {
			return false
		}
	}

	return true
}

func main() {
    // Constructing the first binary tree
    root1 := &TreeNode{
        Val: 3,
        Left: &TreeNode{
            Val: 1,
            Left: &TreeNode{
                Val: 1,
            },
            Right: &TreeNode{
                Val: 2,
            },
        },
        Right: &TreeNode{
            Val: 8,
            Left: &TreeNode{
                Val: 5,
            },
            Right: &TreeNode{
                Val: 13,
            },
        },
    }

    // Constructing the second binary tree
    root2 := &TreeNode{
        Val: 8,
        Left: &TreeNode{
            Val: 3,
            Left: &TreeNode{
                Val: 1,
                Left: &TreeNode{
                    Val: 1,
                },
                Right: &TreeNode{
                    Val: 2,
                },
            },
            Right: &TreeNode{
                Val: 5,
            },
        },
        Right: &TreeNode{
                Val: 13,
				},
    }

    fmt.Println(isSameSequence(root1, root2)) // Output: true
}

Mutex

Something that we need to take care of when working with GoRoutines is the access to shared memory, were more than one GoRoutine can access the same memory space at the same time, this can lead to great conflicts.

This concept is called mutual exclusion, and it's solved by the use of mutexes, which are used to synchronize access to shared memory.

import ("sync")

var mu sync.Mutex

It has two methods, Lock and Unlock, which are used to protect the shared memory:

func safeIncrement() {
	mu.Lock() // lock the shared memory
	defer mu.Unlock() // unlock the shared memory when the function returns
	count++ // increment the shared memory
}

Here we are using the defer statement to ensure that the mutex is unlocked when the function returns, even if it panics.

package main

import (
	"fmt"
)

type TreeNode struct {
	Val   int
	Left  *TreeNode
	Right *TreeNode
}

type SequenceCollector struct {
	sequence map[int]bool
}

func isSameSequence(root1, root2 *TreeNode) bool {
	seq1 := &SequenceCollector{sequence: make(map[int]bool)}
	seq2 := &SequenceCollector{sequence: make(map[int]bool)}

	traverse(root1, seq1)
	traverse(root2, seq2)

	return equal(seq1.sequence, seq2.sequence)
}

func traverse(node *TreeNode, seq *SequenceCollector) {
	if node == nil {
		return
	}

	seq.sequence[node.Val] = true

	traverse(node.Left, seq)
	traverse(node.Right, seq)
}

func equal(seq1, seq2 map[int]bool) bool {
	if len(seq1) != len(seq2) {
		return false
	}

	for val := range seq1 {
		if !seq2[val] {
			return false
		}
	}

	return true
}

func main() {
	// Construyendo el primer árbol binario
	root1 := &TreeNode{
		Val: 3,
		Left: &TreeNode{
			Val: 1,
			Left: &TreeNode{
				Val: 1,
			},
			Right: &TreeNode{
				Val: 2,
			},
		},
		Right: &TreeNode{
			Val: 8,
			Left: &TreeNode{
				Val: 5,
			},
			Right: &TreeNode{
				Val: 13,
			},
		},
	}

	// Construyendo el segundo árbol binario
	root2 := &TreeNode{
		Val: 8,
		Left: &TreeNode{
			Val: 3,
			Left: &TreeNode{
				Val: 1,
				Left: &TreeNode{
					Val: 1,
				},
				Right: &TreeNode{
					Val: 2,
				},
			},
			Right: &TreeNode{
				Val: 5,
			},
		},
		Right: &TreeNode{
			Val: 13,
		},
	}

	fmt.Println(isSameSequence(root1, root2)) // Salida: true
}