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.
Recommended Structure:
- 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)
- web (contains the application logic)
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:
-
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"]) }
-
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
}