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
- 2. Meet the CLI
- 3. Comparing Different Syntax's
- 4. Stdlib: definitely an AWESOME toolkit
- 5. Tests in Go are THAT SIMPLE
- 6. Beware Cyclic Imports
- 7. Defer this, defer that... But, what is a Defer?
- 8. Error Management for Noobs
- 9. Conclusion feat: Low Latency Coding Challenge
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
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
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
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
Here's our app.go
content:
package main
import(
"fmt"
)
func main () {
fmt.Println("whats up! don't forget to like this article <3")
}
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 ofstruct
. - [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 // ✅
}
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
}
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');
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}
}
}
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! )
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!]
}
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.
-
packages:
- 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...]
-
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
-
packages:
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.")
}
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
}
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)
}
}
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)
}
})
}
}
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
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...
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
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();
}
}
}
}
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.
}
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
}
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.
// ...
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.
}
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 (6)
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
orconst
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.
caraca quanta coisa, top!!!!!!!!!!!!!!
Now I know where to start studying GoLang!! Thanks cousin!
wow so cool! nice article cousin
Thanks for the article, Daniel! You inspire us all.
That's an awesome article! Reading this makes me get hype for studying Golang. Congratulations for your experience and keep sharing your journey!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.