DEV Community

Cover image for This is all what I've learned about Go in TWO Weeks!
Daniel Reis
Daniel Reis

Posted on

This is all what I've learned about Go in TWO Weeks!

What is your approach when you need to learn something new? I have a very specific one and once again I tested it while learning Golang!

There's too much content to talk about, but my aim here is to list things that I found useful and that I specifically took the time to learn properly.

Table of Contents

1. Prologue

For the last 2 weeks I've been learning and building small applications with Golang. At the moment it's been almost 50h of code through many livestreams and it's been pretty awesome to learn something that I previously had a some small issues with the language.

In this two weeks journey I've crafted:

  • A small and REALLY simple shell
  • A Redis Basic implementation
  • HTTP 1.1 protocol implementation
  • A DNS server implementation
  • and a job test for a really cool company (which will be available at the end of this article).

And all this because my boss asked me, once again, to learn a new technology to work on some ScyllaDB PoC's and demos... I wasn't too happy with the decision, but well, it's my job.

During the last year I've been studying Rust, and it's probably still too complex for me, but I've learnt some really cool concepts that made my switch to Go work like a charm!

In this article I'll give you some tips and advice to speed up your learning flow.

2. Meet the CLI

I'm a PHP developer and I'm used to the BEST CLI ever made (yes, it's Artisan), however through my journey as a developer I've been through awesome projects many of which have been..:

  • Cargo (Rust)
  • NPM (JS)
  • Composer (PHP)
  • and so on...

When I got to the Go environment, it started as a real problem. At least for me, the developer experience of Golang in terms of tools and documentation could be much better. Thinking about this, I decided to go through 3 commands that you HAVE TO LEARN at the beginning.

Remember: this is just a walkthrough with my own explanation of things. If you want detailed information, open the docs :)
Also: go docs sucks please someone put a syntax highlighter there

2.1 CLI: go mod

Depending on whether you want to modularise your application or have an organised environment, this will be the most useful command at first.

The go mod command manages all the dependencies within your project and also takes care of autoremoving anything that's no longer used.

First, inside your new empty folder, let's init a new module inside the project with go mod init:

mkdir go-fodase
cd go-fodase

# go mod init <module-name>
go mod init github.com/danielhe4rt/go-fodase
Enter fullscreen mode Exit fullscreen mode

This will create a new file in the project root called go.mod, which is basically the contents at the moment:

  • The module name
  • Your Go version

Here's the file, if you want to check it yourself:

# folder: ~/go-fodase
cat go.mod

# module github.com/danielhe4rt/gofodase
# 
# go 1.23.2
Enter fullscreen mode Exit fullscreen mode

After that, the next thing I really liked was the go mod tidy, which basically adds any missing dependencies and removes unused ones.

go mod tidy
Enter fullscreen mode Exit fullscreen mode

This second one is just to keep into your mind that this exists and it's really useful! Probably your environment will run it EVERY TIME and you will get used to see imports vanishing :p

2.2 CLI: go run

This is probably the most common command you'll use, since you HAVE to run your project, but here's how it works:

  • You should point to the file that contains the main() function.
  • This file doesn't have to be in the root of your folder.

The most important thing to remember about this command is that when you run the go run command, it will look for the go.mod file in your current directory and use it as the basis for mapping your whole project (imports, packages, etc). Here's some examples:

# go run <your-main-file>

# Environment 1
# .
# ├── go.mod
# └── main.go
go run app.go

# Environment 2
# .
# ├── go.mod
# └── src
#     └── main.go
go run src/app.go
Enter fullscreen mode Exit fullscreen mode

Here's our app.go content:

package main

import(
    "fmt"
)

func main () {
    fmt.Println("whats up! don't forget to like this article <3")
}

Enter fullscreen mode Exit fullscreen mode

Now you know the basics to execute an project! Literally, hello world!

3. Comparing Different Syntax's:

My problem with Go has always been the way it's written, but after hours of coding I realised that it's simpler than I thought. As you might have guessed, I have a strong background in PHP and some experience with Rust.

When I started to learn Rust in 2023, fortunately a guy I'm a big fan of, Nuno Maduro (Laravel), gave a talk called "PHP for Rust Developers", which gave me some basic introduction to the syntax and gave me some breathing space while I was completely STOMPED by Rust.

Anyway, it was useful to me at the time, so why not do some comparisons?

3.1 Syntax: Classes/Structs and the API Encapsulation

In OOP we have classes, which is a really cool way of abstracting your code into small pieces, and you have something "like that". Golang can be seen as an odyssey, because it can be an epic development to turn the environment into whatever you want it to be.

Remember, Golang is a "high level language" that provides a "system level" syntax that allows you to easily work with "low level" implementations.

Under the Go syntax, you can

  • [Struct] Define a struct by prefixing it with type, adding your "class/struct" name and then adding a suffix of struct.
  • [Encapsulation] Define the exposure of your class/structure related elements by starting them with UpperCase or LowerCase names.
    • [Visibility: "public"]: Set the item name to uppercase.
    • [Visibility: "private/protected"]: Set the item name in lower case.

And you can use it for: Structs, Struct Fields, Struct Methods. Take a closer look:

// -----------------------
// file: src/users/user.go
package users

type User struct {  
    name string  // starts with uppercase: public
    Age  uint8  // starts with uppercase: public
}  

// Starts with lowercase: private
func (u *User) getName() string {  
    return u.name  
}  

// Starts with uppercase: public
func (u *User) Greetings() string {  
    cheering := "Don't forget to follow me back!"

    // Can consume same struct functions.
    return fmt.Sprintf("Hey, %v (%v)! %v !", u.getName(), u.Age, cheering)
} 


// -----------------
// file: src/main.go
package main  

import "github.com/danielhe4rt/go-fodase/src/users"  

func main() {  
    // ❌ You CAN'T start 'name'. Use a Setter Function for it.    
    // user := users.User{    
    //     name: "danielhe4rt", // ❌
    //     Age:  25,            // ✅
    // }

    // ✅ Now you're only initializing what you need.    
    user := users.User{Age: 25}  

    // Methods Calls  
    user.SetName("danielhe4rt") // ✅
    user.getName()              // ❌
    user.Greetings()            // ✅

    currentName := user.name() // ❌ 
    currentAge := user.Age     // ✅ 
}
Enter fullscreen mode Exit fullscreen mode

In Rust, you have an more explicit approach (more oop like languages) where:

  • [Struct] Define an struct using the prefix struct, adding your "Class/Struct" name and that's it.
  • [Encapsulation] If you want something to be public to other "crates", you should add the pub prefix in the part of the code you want to expose.
// file: src/users/mod.rs

// Make the struct `User` public
pub struct User {
    // Private field: accessible only within the `users` module
    name: String,
    // Public field: accessible from other modules
    pub age: u8,
}

impl User {
    // Public method to create a new User with a name and age
    pub fn new(age: u8) -> Self {
        User {
            name: String::new(), // Initialize with an empty name
            age,
        }
    }

    // Public setter method to set the private `name` field
    pub fn set_name(&mut self, name: String) {
        self.name = name;
    }

    // Private method to get the name
    fn get_name(&self) -> &str {
        &self.name
    }

    // Public method that uses both public and private fields/methods
    pub fn greetings(&self) -> String {
        let cheering = "Don't forget to follow me back!";
        format!(
            "Hey, {} ({} years old)! {}",
            self.get_name(),
            self.age,
            cheering
        )
    }
}

// file: src/main.rs
mod users;  

use crate::users::User;  

fn main() {
    // ❌ You CAN'T directly set the private `name` field
    // let user = User {
    //     name: String::from("danielhe4rt"), // ❌
    //     age: 25,                           // ✅
    // };

    // ✅ Initialize the User using the constructor
    let mut user = User::new(25);

    // ✅ Use the setter method to set the private `name`
    user.set_name(String::from("danielhe4rt"));

    let greeting = user.greetings();    // ✅ Call the public `greetings` method
    let current_age = user.age;         // ✅ Access the public `age` field directly
    let current_name = user.name;       // ❌ You CAN'T access the private `name` field directly
    let current_name = user.get_name(); // ❌ You CAN'T call the private `get_name` method directly
}
Enter fullscreen mode Exit fullscreen mode

I'd like to make things explicit like PHP, Java and so on but if you stop to think is LESS CODE to write, but it also impacts the readability.

3.2 Syntax: Interface Implementation is WEIRD AS FUC*

To be really honest, I'm the kind of person who would try to put, I don't know... LARAVEL in Go Environment, but that was already done in Goravel. Anyway, I really like the idea of working with "Interface/Contract Driven Development", and for the first time I got stuck with it in a language.

In Go, interfaces aren't "implemented" in a structure/class, and for an OOP guy like me, it's just crazy to have such a design decision fit into my head. Have a look at what is expected:

interface OAuthContract {
    public function redirect(string $code); 
    public function authenticate(string $token);
}

class GithubOAuthProvider implements OAuthContract{
    public function redirect(string $code) {}
    public function authenticate(string $token) {}
}

class SpotifyAuthProvider implements OAuthContract{
    public function redirect(string $code) {}
    public function authenticate(string $token) {}
}

function authenticate(string $routeProvider): OAuthContract {
    return match($provider) {
        'github' => new GithubOAuthProvider(),
        'spotify' => new SpotifyAuthProvider(),
        default => throw OAuthProviderException::notFound(),
    };
}

authenticate('github');
Enter fullscreen mode Exit fullscreen mode

Now, when it comes to go: you don't have this explicit implementation of an "interface" inside a structure, and that's, hmm... weird? Instead, you just implement the interface's required methods, which go will check for you at compile time. It's fair to know that this is a compiled language and it should never be a problem, but I'm talking about my perspective with Developer Experience!

import (
    "errors"
    "fmt"
)

type OAuthContract interface {
    Redirect(code string) error
    Authenticate(token string) error
}

// GithubOAuthProvider implements the OAuthContract interface for GitHub.
type GithubOAuthProvider struct{}

// Redirect handles the redirection logic for GitHub OAuth.
func (g *GithubOAuthProvider) Redirect(code string) error {
    // Implement GitHub-specific redirection logic here.
    return nil
}

// Authenticate handles the authentication logic for GitHub OAuth.
func (g *GithubOAuthProvider) Authenticate(token string) error {
    // Implement GitHub-specific authentication logic here.
    return nil
}

// SpotifyOAuthProvider implements the OAuthContract interface for Spotify.
type SpotifyOAuthProvider struct{}

// Redirect handles the redirection logic for Spotify OAuth.
func (s *SpotifyOAuthProvider) Redirect(code string) error {
    // Implement Spotify-specific redirection logic here.
    fmt.Printf("Redirecting to Spotify with code: %s\n", code)
    return nil
}

// Authenticate handles the authentication logic for Spotify OAuth.
func (s *SpotifyOAuthProvider) Authenticate(token string) error {
    // Implement Spotify-specific authentication logic here.
    fmt.Printf("Authenticating Spotify token: %s\n", token)
    return nil
}

// AuthenticateFactory is a factory function that returns an instance of OAuthContract
func AuthenticateFactory(provider string) (OAuthContract, error) {
    switch provider {
    case "github":
        return &GithubOAuthProvider{}, nil
    case "spotify":
        return &SpotifyOAuthProvider{}, nil
    default:
        return nil, &OAuthProviderError{Provider: provider}
    }
}
Enter fullscreen mode Exit fullscreen mode

In any case, with some time coding in the language you will get used with it. Now, let's talk about what the base environment offers to you without download anything!

4. Stdlib: definitely an AWESOME toolkit

Now I'm talking about everything that Go serves you with the Standard Library, without download an third party package. Here's some chronological timeline for you:

  • 1st day: WHAT? WHY NOT LIKE JS/Java IN THE MATTER THAT THE TYPE CARRIES ALL METHODS? (And I hate both of them)
  • 1st week: Wait, maybe that's good shit (after understand the packages for primitive types)
  • 2nd week: WHAT? WHY NOT OTHER LANGS HAVE SUCH A GOOD LIBRARIES BY DEFAULT?

I'm not joking about that, every day that I explore go I found some cool library under the standard ones. So, let's start talking about primitive types.

4.1 Packages: Primitive Types

Like PHP, and unlike many other languages (Rust, Java, JS, etc), Golang needs "helper" functions to perform most of the related type operations. We can think of them as "anemic" types, since they don't have "utility" attached to them.

// PHP Example (with a REALLY UGLY API)
// String manipulation using built-in PHP functions
$str = "Hello, World!";

// Check if the string contains a substring
$contains = strpos($str, "World") !== false;
echo "Contains 'World': " . ($contains ? "true" : "false") . "\n"; // Output: true

// Convert the string to uppercase
$upper = strtoupper($str);
echo "Uppercase: " . $upper . "\n"; // Output: HELLO, WORLD!

// Split the string by comma
$split = explode(",", $str);
echo "Split by comma: ";
print_r($split); // Output: Array ( [0] => Hello  [1] =>  World! )
Enter fullscreen mode Exit fullscreen mode

So if you're working with a "String" type, you have other packages like strconv or strings to manipulate it! But here's a golden rule to never forget which package to look at: if your type is string, look for the same package with a pluralised name!

In a nutshell, this will give you functions related to []Type and Type:

  • String type -> import ("strings") for operations like: Contains(), Upper(), Split() ...
  • Bytes type -> import ("bytes") for operations like Include(), Compare(), Split() ...
  • and so on!

Take a look at the code, so you can validate by yourself:

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    // String manipulation using the "strings" package
    str := "Hello, World!"

    // Check if the string contains a substring
    contains := strings.Contains(str, "World")
    fmt.Printf("Contains 'World': %v\n", contains) // Output: true

    // Convert the string to uppercase
    upper := strings.ToUpper(str)
    fmt.Printf("Uppercase: %s\n", upper) // Output: HWith this intELLO, WORLD!

    // Split the string by comma
    split := strings.Split(str, ",")
    fmt.Printf("Split by comma: %v\n", split) // Output: [Hello  World!]
}
Enter fullscreen mode Exit fullscreen mode

That's supposed to be simple, but I struggled with that for a while until gets into my head. Maybe using Laravel and their helper functions for too many years made me forget how tough is to code without a Framework :D

4.2 Packages: Useful Stuff

While I was exploring tools and projects, I got a really good introduction to many projects and I'd like to list each of them and the libs I've used:

  • Build your own Shell Challenge:
    • packages:
      • fmt: I/O library (Scan/Read and Write stuff on your screen)
      • os: functions and helpers that talks directly with your Operational System.
      • strconv: cast specific data-types to string or cast string to any defined type.
  • Build your own (HTTP|DNS) Server Challenge:
    • packages:
      • net: integration layer with network I/O protocols such as HTTP, TCP, UDP and Unix Domain Sockets
      • [previous packages...]
  • Mid Level Homework Task assignment?
    • packages:
      • flag: Captures your CLI arguments into variables
      • log: Adds Log's channels to your application
      • crypto/rand: Secure Cryptographic Random Generator
      • math/rand: Math Numbers Random Generator
      • time: Duration/Time Lib

Here's a scrollable view of all the package implementations so you can check them out. There are PLENTY of cool std packages that can be cited here.

ATTENTION: that's a LOT OF CODE! :p
Don't forget to comment your favorite features (besides goroutines and channels) :p

package main

import (
    crypto "crypto/rand"
    "encoding/hex"
    "flag"
    "fmt"
    "log"
    "math/rand"
    "net/http"
    "os"
    "strconv"
    "time"
)

// =========================
// Example 1: fmt Package
// =========================

// fmtExample demonstrates basic input and output operations using the fmt package.
func fmtExample() {
    fmt.Println("=== fmt Package Example ===")
    var name string
    var age int

    // Prompt the user for their name
    fmt.Print("Enter your name: ")
    _, err := fmt.Scanln(&name)
    if err != nil {
        fmt.Println("Error reading name:", err)
        return
    }

    // Prompt the user for their age
    fmt.Print("Enter your age: ")
    _, err = fmt.Scanln(&age)
    if err != nil {
        fmt.Println("Error reading age:", err)
        return
    }

    // Display the collected information
    fmt.Printf("Hello, %s! You are %d years old.\n\n", name, age)besides
}

// =========================
// Example 2: os Package
// =========================

// osExample showcases how to interact with the operating system, such as reading environment variables and exiting the program.
func osExample() {
    fmt.Println("=== os Package Example ===")

    // Retrieve the HOME environment variable
    home := os.Getenv("HOME")
    if home == "" {
        fmt.Println("HOME environment variable not set.")
        // Exit the program with a non-zero status code to indicate an error
        // Uncomment the next line to enable exiting
        // os.Exit(1)
    } else {
        fmt.Printf("Your home directory is: %s\n", home)
    }

    // Demonstrate exiting the program successfully
    // Uncomment the next lines to enable exiting
    /*
        fmt.Println("Exiting program with status code 0.")
        os.Exit(0)
    */
    fmt.Println()
}

// =========================
// Example 3: strconv Package
// =========================

// strconvExample demonstrates converting between strings and other data types.
func strconvExample() {
    fmt.Println("=== strconv Package Example ===")

    // Convert string to integer
    numStr := "456"
    num, err := strconv.Atoi(numStr)
    if err != nil {
        fmt.Println("Error converting string to int:", err)
        return
    }
    fmt.Printf("Converted string '%s' to integer: %d\n", numStr, num)

    // Convert integer back to string
    newNumStr := strconv.Itoa(num)
    fmt.Printf("Converted integer %d back to string: '%s'\n\n", num, newNumStr)
}

// =========================
// Example 4: net Package
// =========================

// netExample demonstrates making a simple HTTP GET request using the net package.
func netExample() {
    fmt.Println("=== net Package Example ===")

    // Make an HTTP GET request to a public API
    resp, err := http.Get("https://api.github.com")
    if err != nil {
    let greeting = user.greetings();
        fmt.Println("Error making HTTP GET request:", err)
        return
    }
    defer resp.Body.Close()

    // Display the HTTP status code
    fmt.Printf("Response Status: %s\n\n", resp.Status)
}

// =========================
// Example 5: flag Package
// =========================

// flagExample demonstrates how to capture command-line arguments using the flag package.
func flagExample() {
    fmt.Println("=== flag Package Example ===")

    // Define command-line flags
    name := flag.String("name", "World", "a name to say hello to")
    age := flag.Int("age", 0, "your age")

    // Parse the flags
    flag.Parse()

    // Use the flag values
    fmt.Printf("Hello, %s!\n", *name)
    if *age > 0 {
        fmt.Printf("You are %d years old.\n\n", *age)
    } else {
        fmt.Println()
    }
}

// =========================
// Example 6: log Package
// =========================

// logExample demonstrates logging messages to a file using the log package.
func logExample() {
    fmt.Println("=== log Package Example ===")

    // Open or create a log file
    file, err := os.OpenFile("app.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        log.Fatalf("Failed to open log file: %s", err)
    }
    defer file.Close()

    // Set the log output to the file
    log.SetOutput(file)

    // Log messages
    log.Println("Application started")
    log.Printf("Logging an event at %s", "some point")
    log.Println("Application finished")

    fmt.Println("Logs have been written to app.log\n")
}

// =========================
// Example 7: crypto/rand Package
// =========================

// cryptoRandExample demonstrates generating a secure random string using the crypto/rand package.
func cryptoRandExample() {
    fmt.Println("=== crypto/rand Package Example ===")

    // Generate a secure random string of 16 bytes (32 hex characters)
    randomStr, err := generateSecureRandomString(16)
    if err != nil {
        fmt.Println("Error generating secure random string:", err)
        return
    }
    fmt.Println("Secure Random String:", randomStr, "\n")
}

// generateSecureRandomString generates a hexadecimal string of the specified byte length using crypto/rand.
func generateSecureRandomString(length int) (string, error) {
    bytes := make([]byte, length)
    _, err := crypto.Read(bytes)
    if err != nil {
        return "", err
    }
    return hex.EncodeToString(bytes), nil
}

// =========================
// Example 8: math/rand Package
// =========================

// mathRandExample demonstrates generating pseudo-random numbers and shuffling a slice using the math/rand package.
func mathRandExample() {
    fmt.Println("=== math/rand Package Example ===")

    // Seed the random number generator to ensure different outputs each run
    rand.Seed(time.Now().UnixNano())

    // Generate a random integer between 0 and 99
    randomInt := rand.Intn(100)
    fmt.Printf("Random integer (0-99): %d\n", randomInt)

    // Generate a random float between 0.0 and 1.0
    randomFloat := rand.Float64()
    fmt.Printf("Random float (0.0-1.0): %f\n", randomFloat)

    // Shuffle a slice of integers
    nums := []int{1, 2, 3, 4, 5}
    rand.Shuffle(len(nums), func(i, j int) {
        nums[i], nums[j] = nums[j], nums[i]
    })
    fmt.Printf("Shuffled slice: %v\n\n", nums)
}

// =========================
// Example 9: time Package
// =========================

// timeExample demonstrates working with current time, formatting, parsing, sleeping, and measuring durations using the time package.
func timeExample() {
    fmt.Println("=== time Package Example ===")

    // Current time
    now := time.Now()
    fmt.Println("Current time:", now)

    // Formatting time
    formatted := now.Format("2006-01-02 15:04:05")
    fmt.Println("Formatted time:", formatted)

    // Parsing time from a string
    parsed, err := time.Parse("2006-01-02", "2024-10-25")
    if err != nil {
        fmt.Println("Error parsing time:", err)
        return
    }
    fmt.Println("Parsed time:", parsed)

    // Sleep for 2 seconds
    fmt.Println("Sleeping for 2 seconds...")
    time.Sleep(2 * time.Second)
    fmt.Println("Awake!")

    // Measuring duration
    start := time.Now()
    time.Sleep(1 * time.Second)
    elapsed := time.Since(start)
    fmt.Printf("Elapsed time: %s\n\n", elapsed)
}

// =========================
// Main Function
// =========================

func main() {
    // Execute each example function sequentially
    fmtExample()
    osExample()
    strconvExample()
    netExample()
    flagExample()
    logExample()
    cryptoRandExample()
    mathRandExample()
    timeExample()

    fmt.Println("All examples executed.")
}
Enter fullscreen mode Exit fullscreen mode

Seriously, that's just amazing! So, lets keep going for tests now.

5. Tests in Go are THAT SIMPLE

In my second project using Go, I saw an opportunity to learn tests while creating Requests and Responses objects. Inside the PHP environment, you are probably using a 3rd party library like PHPUnit or Pest. Right? Inside the Go environment, this is EASY! All you need to do is:

  • Create a file inside a package: In person.go you will write the functions you want to test;
  • Create a test file for your package :** create a file called person_test.go and start writing your own tests!

Let's say we have requests.go and requests_test.go in our package folder, where requests.go is:

package request  

import (  
    "bytes"  
    "fmt")  

type VerbType string  

const (  
    VerbGet  VerbType = "GET"  
    VerbPost VerbType = "POST"  
)  

type Request struct {  
    Verb    VerbType  
    Version string  
    Path    string  
    Headers map[string]string  
    Params  map[string]string  
    Body    string  
}  

func NewRequest(payload []byte) Request {  

    payloadSlices := bytes.Split(payload, []byte("\r\n"))  

    verb, path, version := extractRequestLine(payloadSlices[0])  
    headers := extractHeaders(payloadSlices[1 : len(payloadSlices)-2])  
    body := payloadSlices[len(payloadSlices)-1]  
    req := Request{  
       Verb:    VerbType(verb),  
       Version: version,  
       Path:    path,  
       Headers: headers,  
       Params:  map[string]string{},  
       Body:    string(body),  
    }  
    return req  
}  

func extractHeaders(rawHeaders [][]byte) map[string]string {  
    headers := make(map[string]string)  

    for _, headerBytes := range rawHeaders {  
       fmt.Printf("%v\n", string(headerBytes))  
       data := bytes.SplitN(headerBytes, []byte(": "), 2)  

       key, value := string(data[0]), string(data[1]) // Accept (?)  
       headers[key] = value  
    }  

    return headers  
}  

func extractRequestLine(requestLine []byte) (string, string, string) {  

    splitRequestLine := bytes.Split(requestLine, []byte(" "))  
    verb := string(splitRequestLine[0])  
    path := string(splitRequestLine[1])  
    version := string(splitRequestLine[2])  
    return verb, path, version  
}
Enter fullscreen mode Exit fullscreen mode

5.1 Tests: Basic Testing

A test in Go is considered PASSED (green) if (t *Testing.T).Errorf() is not called within your test function. It also follows the same concept of encapsulation introduced earlier:

  • Test Functions starting with uppercase are identified by the Test Runner
  • Test Functions starting with lowercase are ignored (usually helper functions)
package request

import (
    "reflect"
    "testing"
)

func TestNewRequest(t *testing.T) {
    // Prepare
    payload := []byte("GET /index.html HTTP/1.1\r\nHost: localhost:4221\r\nUser-Agent: curl/7.64.1\r\nAccept: */*\r\n\r\n")

    // Act
    got := NewRequest(payload);

    // Assert 
    want := Request{
        Verb:    VerbGet,
        Version: "HTTP/1.1",
        Path:    "/index.html",
        Headers: nil,
    }

    if !reflect.DeepEqual(got, want) {
        t.Errorf("FAILED! NewRequest() = %v, want %v", got, want)
    }
}
Enter fullscreen mode Exit fullscreen mode

You can do your own helper functions to test. Just make sure to not trespass the module domain on these tests!

5.2 Tests: Jetbrains Boilerplate

I've been using Goland since day one, so most things have been easier for me. So every time I start a new test, I get this boilerplate with a Unity test structure that runs in parallel (goroutines) by default.

package request

import (
    "reflect"
    "testing"
)

func TestNewRequest(t *testing.T) {
    type args struct {
        payload []byte
    }
    tests := []struct {
        name string
        args args
        want Request
    }{
        {
            name: "Base Request Argless",
            args: args{
                payload: []byte("GET /index.html HTTP/1.1\r\nHost: localhost:4221\r\nUser-Agent: curl/7.64.1\r\nAccept: */*\r\n\r\n"),
            },
            want: Request{
                Verb:    VerbGet,
                Version: "HTTP/1.1",
                Path:    "/index.html",
                Headers: nil,
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := NewRequest(tt.args.payload); !reflect.DeepEqual(got, tt.want) {
                t.Errorf("NewRequest() = %v, want %v", got, tt.want)
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

5.3 Tests: Running Tests

Ok, now we know how easy it is to write tests in Go, but how about running them? Simple task! All you need to do is navigate to the package folder and run:

# ~/go_fodase
cd requests
ls
# requests.go request_test.go
go test # Run all tests inside the package in all files with suffix "_go"
# ...
# PASS
# ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s

go test -run=TestNewRequest # Filter an specific test
# PASS
# ok      github.com/danielhe4rt/go_fodase/requests/request      0.001s
Enter fullscreen mode Exit fullscreen mode

Please write down some tests for your stuff. It's not that hard if you decouple what's needed :p

6. Beware Cyclic Imports

During my last few years of development, I've always tried to modularise all my projects in a way that suits my needs, without getting stuck on "Clean Arch" or "Domain Driven Design" stuff. However, in my first attempts at splitting my packages, I got the "Cyclic Import" error and thought to myself: HOW LONG SINCE I'VE SEEN SOMETHING LIKE THAT?

During my 2 years in PHP, I had the same problem with import hell, where you couldn't not import the same file TWICE in a particular flow. This was before I met the PSR-4 (Autoloading) (which changed my PHP days forever!!) and now, years ago, I'm struggling with this in Go.

Let's consider a scenario of cyclic imports:

// ------------------
// A.php

require_once 'B.php';

class A {
    public function __construct() {
        $this->b = new B();
    }

    public function sayHello() {
        echo "Hello from A\n";
    }
}

// ------------------
// B.php

require_once 'A.php';

class B {
    public function __construct() {
        $this->a = new A();
    }

    public function sayHello() {
        echo "Hello from B\n";
    }
}
// ------------------
// index.php

require_once 'A.php';

$a = new A();
$a->sayHello();
// Fatal error: Uncaught Error: Maximum function nesting level of '256' reached...
Enter fullscreen mode Exit fullscreen mode

When you try to compile something that flags Cyclic Imports in Go, you will receive an error like:

import cycle not allowed
package path/to/packageA
    imports path/to/packageB
    imports path/to/packageA
Enter fullscreen mode Exit fullscreen mode

And in this moment, you have to start breaking down your dependencies/packages in order to avoid it.

TLDR: don't import the same package in a place that will be loaded many times.

7. Defer this, defer that... But, what is a Defer?

I didn't look it up, but it was the first time I'd seen the reserved word defer in a programming language. And since it was not part of the "generic reserved words", I ignored it for a whole week!

Then one of my work mates, Dusan, gave me a memory management lesson in Go after seeing me struggle with the language for a couple of hours. (Yes, this is a shout-out :p)

The thing is: whenever you open a buffer/connection to something, you SHOULD CLOSE IT! I remember when I was working with MapleStory servers (Java) in 2014, the most common problem was memory leaks, simply because the developers didn't close the connections to the DB.

This is OK to FORGET! But it's not OK to pass in the Code Review LOL

Here's an example in Java:

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;
import java.sql.SQLException;

public class UnclosedConnectionExample {
    public static void main(String[] args) {
        // Database credentials
        String url = "jdbc:mysql://localhost:3306/mydatabase";
        String user = "username";
        String password = "password";

        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;

        while {
            try {
                // Establishing the connection
                conn = DriverManager.getConnection(url, user, password);
                System.out.println("Database connected successfully.");

                // Creating a statement
                stmt = conn.createStatement();

                // Executing a query
                String sql = "SELECT id, name, email FROM users";
                rs = stmt.executeQuery(sql);

                // Processing the result set
                while (rs.next()) {
                    int id = rs.getInt("id");
                    String name = rs.getString("name");
                    String email = rs.getString("email");

                    System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
                }

                // Intentionally not closing the resources
                // rs.close() // <-------------------
                // This leads to resource leaks
            } catch (SQLException e) {
                // rs.close() // <-------------------
                e.printStackTrace();
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

While coding Golang, they give this defer attribute for you to remember to close your stuff right after opening it.

Defer stands for "Deference" which is a way to "Clean" your resources after the execution of that specific portion of code ends.

import(
    "github.com/gocql/gocql"
)

func main() {
    cluster := gocql.NewCluster("localhost:9042")  
    session, err := cluster.CreateSession()  

    if err != nil {  
        log.Fatal("Connection Refused!")  
    }  
    defer session.Close() // After the function ends, the method will be called

    // Migrate stuff  
    fmt.Println("Migrating Tables...")
    database.Migrate(session)
    // ... rest of your code

    // After the last line of code, the function `session.Close()` will run without you trigger it. 
}

Enter fullscreen mode Exit fullscreen mode

You can also have many defer's within a function and the DEFER ORDER matters! If you defer database2 and then defer database1, both processes will be cleaned in the same order.

import(
    "github.com/gocql/gocql"
)

func main() {
    cluster := gocql.NewCluster("localhost:9042")  

    loggingSession, _ := cluster.CreateSession()  
    appSession, _ := cluster.CreateSession()  

    defer appSession.Close()
    defer loggingSession.Close()

    // ... rest of your code
    // Defer Order
    // ...
    // appSession.close() 1st
    // loggingSession.close() 2nd
}
Enter fullscreen mode Exit fullscreen mode

This is a really simple way to not fuck up prevent your project from having a memory leak. Please remember to use it whenever you stream anything.

8. Error Management for Noobs

Error handling at first will be something like: check if the function you're using returns an error type and validate it EVERY FUCKING TIME! Here's an example of what I'm talking about:

cluster := gocql.NewCluster("localhost:9042")  
appSession, err := cluster.CreateSession()  

// NIL STUFF
if err != nil {
    // do something
    log.Fatal("Well, something went wrong.")
}
defer appSession.close()
// Keep your flow. 
// ...
Enter fullscreen mode Exit fullscreen mode

To be honest, I HATE this syntax. However, it's part of the language and will be something you'll come across during your days of coding.

Functions with errors can return error or (T, error), and fortunately Go will not let you forget that.

func SumNumbers(a, b int) (int, error) {
    result := a + b 
    if result < 0 {
        return nil, fmt.Errorf("The result is negative.")
    }

    return result, nil
}

// Using it
* always return when possible
    * spam the type error 
result, err := SumNumbers(-11, 5);

if err != nil {
    log.Fatal(err) // Ends the app or handle in the way you want.
}
Enter fullscreen mode Exit fullscreen mode

Spam your code with err != nil and you will be fine, I promise! :D

9. Conclusion feat: Low Latency Coding Challenge

Aside from all the stress and hours spent trying to understand the environment, it was a cool challenge to learn a new language together with my Twitch viewers. Many of them have been asking me for a long time to check it out and here we are.

All of these points reflect my personal development experience with the language, and the goal was to share things I've gone through during these 2 weeks of studying it.

9.1 Bonus: Coding Challenge

Recently I was challenged by my teammate to complete a ScyllaDB challenge and it taught me a lot about parallelism, pools and rate limiting. This is the kind of challenge that many companies face to make their products perform better!

The goal of the challenge is to create a small Go command line application that inserts some random data into ScyllaDB while rate limiting the number of requests.

You can find the repository challenge here: github.com/basementdevs/throttling-requests-scylla-test. We're also hiring! You can find the open positions in our careers section!

Thank you for reading! I hope this article has provided valuable insights into learning Golang. Feel free to share your thoughts or experiences.

Top comments (2)

Collapse
 
danielboll profile image
Daniel Boll

I've been learning golang recently too, very funny to see some takes that we have in common, one that you didn't mentioned your opinion is the Walrus operator for assignment with initialization, imho I don't like that we don't have some prefix like var or const or whatever prior to the variable name, it's way less clear where variables are being instantiated versus where they are being modified, which tends to occur a little bit more as code in go is more often mutable than in Rust.

Great post as always.

Collapse
 
leonardorafaeldev profile image
Leonardo Rafael Dev

wow so cool! nice article cousin

Some comments may only be visible to logged-in visitors. Sign in to view all comments.