Learning Go: POV of a JavaScript Developer PT. 2

Talut Salako

learningGo

1265 Words … ⏲ Reading Time:5 Minutes, 45 Seconds

2024-10-10 13:24 +0000


Variable declaration

variable declaration is straight forward in Go.

var/const varName type

If a variable is declared without an initial value, Go automatically assigns it a default value based on its type. Additionally, there is a shorthand declaration and assignment syntax that uses :=.

package main

const firstName string // correct
const lastName = "Salako" // correct

var age = 16 // correct
var role string = "Backend Developer" // correct

// This will cause an error because it's outside of a function.
// alias := "plutack" // wrong

func main() {
    realAlias := "plutack" // correct
}

The shorthand form automatically assigns the var keyword to the declared variable with its type inferred. It is also worth noting the shorthand syntax cannot be used outside a function.

Function Declaration in Go

The syntax for declaring a function in Go differs significantly from JavaScript, primarily in its use of types and the absence of curly braces for defining function parameters. The basic syntax for declaring a function in Go is as follows:

func functionName(parameter1 type1, parameter2 type2) returnType {
    // function body
}

Error Handling Pattern

When defining a function that can encounter an error, the typical signature includes an additional return value of type error. By convention, the first return value is usually the desired result, while the second return value is the error, if any. This allows callers of the function to easily check for errors and handle them appropriately.

Here’s a simple example:

package main

import (
    "fmt"
    "errors"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero is not allowed")
    }
    return a / b, nil // nil indicates no error
}

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }

    // Demonstrating error handling for division by zero
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

Returning Multiple Values

One of the distinctive features of Go is its ability to return multiple values from a function without requiring an object as is common in JavaScript. This capability enhances the language’s flexibility and makes handling different outcomes from a function more straightforward.

In Go, a function can be defined to return multiple types of values. This is useful for scenarios where a function might need to return both a result and an error, or when it produces multiple outputs that can be handled simultaneously.

package main

import "fmt"

// Function that returns multiple values
func getPostInfo() (author, address, jobTitle string) {
    address = "https://blog.talut.tech"
    author = "Talut Salako"
    jobTitle = "Backend Engineer"
    return
}

func main() {
    myName, postDomain, role := getPostInfo() // Receiving multiple return values
    fmt.Printf("Name: %s, url: %s, profession: %s\n", myName, postDomain, role)
}

Returning Pointers Instead of Values

In Go, functions can return pointers instead of values, which is a common practice for several reasons. When a function returns a pointer, it provides a reference to the original data instead of a copy, which can have performance benefits and allow for more flexible data manipulation.

Why Return Pointers?

  1. Performance: When you return a large struct or array by value, a copy of that data is made, which can be inefficient in terms of memory and CPU usage. By returning a pointer, you avoid this overhead, as only the address of the original data is returned.

  2. Mutability: Returning a pointer allows the caller to modify the original data. This is particularly useful in situations where you want to update the state of a struct or when managing resources like slices and maps.

  3. Nil Checks: Pointers can also be used to indicate the absence of a value. A nil pointer can signal that an operation failed or that a particular resource is unavailable, allowing for more explicit error handling.

An implemention of this is shown in the later part of this post

Structs

Unlike JavaScript, where the “class” keyword and its associated concepts exist, Go approaches object-oriented design differently. Here’s an example to illustrate this distinction:

class Account {
	constructor (deposit, accountType){
		if (deposit < 2000){
			throw new Error("Account cannot be created with less than 2000")
		}
		this._balance = deposit
		if (accountType === "fixedDeposit"){
			this.interestRate = 10
		}
		if (accountType === "savings"){
			this.interestRate = 2
		}
		// more code
	
	}
	getBalance(){
		return this._balance
	}
}

const myAccount = new Account(2000)

console.log(myAccount.getBalance()) //  2000

In Go , a typical implementation would look like thus;

package account

import (
	"fmt"
	"sync"
)

var (
	lastRegisteredId int // this initializes to 0
	mu               sync.Mutex
)

type (
	money   float64
	Account struct {
		Id           int
		Name         string
		Balance      money
		InterestRate int
		accountType  string
	}
)

func New(name string, balance money, accountType string) (*Account, error) {
	mu.Lock()
	defer mu.Unlock()

	if balance < 2000 {
		return nil, fmt.Errorf("initial balance %.2f is lower than 2000", balance)
	}
	var interestRate int

	if accountType != "fixedDeposit" && accountType != "savings" {
		return nil, fmt.Errorf("%s is an invalid account type", accountType)
	}

	if accountType == "fixedDeposit" {
		interestRate = 5
	}
	if accountType == "savings" {
		interestRate = 2
	}
	lastRegisteredId++

	newAccount := Account{
		Id:           lastRegisteredId,
		Name:         name,
		Balance:      balance,
		InterestRate: interestRate,
		accountType:  accountType,
	}
	return &newAccount, nil
}

func (a *Account) GetBalance() money {
	return a.Balance
}

Notice the distinction?

Field Alignment

An important concept encountered when defining a struct is field alignment. Field alignment is a thing because different types have varying byte sizes in memory. Depending on the arrangement of your fields in the struct definition, you might use more memory than necessary every time you create a new instance of your struct. Practically, this difference in memory usage is often negligible unless the struct contains a large number of fields.

Methods vs Functions

Just like in JavaScript, both concepts exist in Go. However, methods are defined on structs, while in JavaScript, methods are defined within classes.

Type aliasing and Type Definiton

FeatureType AliasingType Definition
Syntaxtype NewType = ExistingTypetype NewType ExistingType
PurposeCreates an alias for an existing type.Creates a new type based on an existing type.
CompatibilityThe new type is interchangeable with the existing type.The new type is distinct and not interchangeable with the existing type.
Type IdentityBoth types have the same identity.The new type has a different identity.
Use CasesSimplifying long type names or enhancing readability.Creating distinct types for better type safety.
Exampletype money = float64type money float64
Type CheckingNo new type; type checks will pass.Type checks will fail when trying to use them interchangeably.
Method SetsSame method set as the original type. (e.g., money can now be passed with all methods that can be used with float64).Can have a different method set.(i.e., methods need to be user-defined).

Defer and Mutex

Notice the new variable mu and the use of the defer keyword. The defer statement is similar to the finally block in JavaScript’s try-catch-finally. It ensures that the line of code with the keyword is executed at the end of the function, regardless of how the function terminates. One key thing to know is that defer statements are stacked rather than queued.

The Mutex allows a lock on the global variable lastRegisteredId to ensure consistency in a synchronous manner.

In the next blog post, we will delve deeper into mutex locks, channels, and goroutines, with fewer comparisons to JavaScript as we progress.

comments powered by Disqus