0%
Reading Settings
Font Size
18px
Line Height
1.5
Letter Spacing
0.01em
Font Family
Table of contents
    blog cover

    Hello Golang: My First Steps to the Language

    Software Engineer
    Software Engineer
    published 2025-10-18 19:05:21 +0700 · 6 mins read
    I’ve worked with Ruby, a programmer’s best friend for its clean and near natural-language syntax, and with JavaScript/TypeScript, which lets you learn once and build anything: backend, frontend, desktop, or even mobile apps. In university, I also used C++ for data structures and algorithms, and Python for training AI models.

    Each language has taught me something new. There’s no need to argue which one is the best. Each has its own strengths and use cases. Recently, I noticed Go (Golang) becoming more popular in the job market for good reasons, so I decided to learn it.

    When I start learning a new programming language, it helps to begin with its core principles: whether it’s compiled or interpreted, statically or dynamically typed, how it manages memory, how it passes data (by value or by reference), ... and what makes it different from what we already know. That mindset comes from a university subject I once learned: Principles of Programming Languages. It trained me to look beyond syntax and understand how each language is designed to think.

    1. Go’s Nature

    Go is a statically typed, compiled language created at Google. It was designed to solve Google’s real-world problems: building large-scale, high-performance systems while keeping codebases clean and easy to maintain.

    Go’s philosophy is simple: fast build, fast run, fewer surprises. It aims to combine C’s performance and efficiency with Python’s readability and developer productivity.

    Here’s what stands out:
    • Static typing: Go checks types at compile time, not runtime — catching many errors early before your code ever runs. Despite being statically typed, Go’s type inference (:=) keeps syntax concise.
    • Compiled: When you run go build, your entire program (including dependencies) compiles into one static binary. No virtual machine, no interpreter, no dependency hell, just a single executable that runs anywhere.
    • Memory management: Go provides automatic garbage collection like Java or Python, but it’s optimized for low latency.
    • Concurrency: Go’s signature feature makes it simple to handle multiple tasks at once.
    • Simplicity by design: Go avoids complexity: no inheritance, no implicit behavior, no hidden magic. Everything is explicit, predictable, and easy to read.
    • Composition over inheritance: Instead of class hierarchies, Go encourages small, composable types you can combine freely.
    • First-class tooling: Tools like gofmt, go test, and go mod are part of the language itself, enforcing consistency and speeding up development.

    2. Setting Up the Environment

    We can follow the official installation guide here:

    Once installed, check our version:
    // language: bash
    go version

    Initialize a new project:
    // language: bash
    mkdir myfirstgo && cd myfirstgo
    go mod init myfirstgo

    Then create a simple main.go file and run it:
    // language: bash
    go run main.go

    Everything in Go starts from the package main and the main() function. We can also compile our program into a binary using:
    // language: bash
    go build

    3. Variables

    Variables

    Go supports both explicit and inferred variable declarations:
    // language: go
    var message string = "Hello, Go!"
    count := 3 // inferred type: int
    The var keyword lets us declare variables with explicit types, while := provides a shorthand declaration with automatic type inference.

    Go’s basic types include:
    • Integers (int, int64)
    • Floating-point numbers (float32, float64)
    • Strings (string)
    • Booleans (bool)

    4. Data Collections

    In Go, data collections are handled through arrays, slices, and maps.

    Array

    An array has a fixed size — once created, it cannot grow or shrink.
    // language: go
    var nums [3]int
    nums[0] = 10
    nums[1] = 20
    nums[2] = 30
    
    fmt.Println(nums) // [10 20 30]

    We can also declare and initialize an array in one line:
    // language: go
    arr := [3]string{"Go", "Rust", "Python"}
    fmt.Println(arr[1]) // Rust

    Arrays are rarely used directly in Go because their size is static. That’s where slices come in.

    Slice

    A slice is a flexible, dynamic view of an array — we can append, cut, and grow it easily.
    // language: go
    langs := []string{"Go", "Ruby"}
    langs = append(langs, "Python")
    
    fmt.Println(langs) // [Go Ruby Python]
    fmt.Println(len(langs)) // 3

    We can also slice existing data:
    // language: go
    nums := []int{10, 20, 30, 40, 50}
    fmt.Println(nums[1:4]) // [20 30 40]

    Slices are built on top of arrays but provide much more convenience. They’re used in most real Go code.

    Map

    A map is a key-value store, similar to dictionaries in Python or hashes in Ruby.
    // language: go
    scores := map[string]int{
      "Alice": 90,
      "Bob":   85,
    }
    
    fmt.Println(scores["Alice"]) // 90
    
    // Add new value
    scores["Charlie"] = 92
    
    // Check existence
    value, ok := scores["Bob"]
    if ok {
      fmt.Println("Bob's score is", value)
    }

    We can delete keys with:
    // language: go
    delete(scores, "Alice")

    5. Control Flow

    If-else
    // language: go
    if x > 0 {
      fmt.Println("positive")
    } else if x < 0 {
      fmt.Println("negative")
    } else {
      fmt.Println("zero")
    }

    Switch-case
    // language: go
    switch day := "saturday"; day {
    case "saturday", "sunday":
      fmt.Println("Weekend")
    default:
      fmt.Println("Weekday")
    }
    Go automatically breaks after each case, and the default case is optional.

    All loops in Go use the for keyword. There’s no separate while or foreach.
    // language: go
    for i := 0; i < 5; i++ {
      fmt.Println(i)
    }

    We can also use for ... range to iterate over collections:
    // language: go
    langs := []string{"Go", "Ruby", "Python"}
    for index, value := range langs {
      fmt.Println(index, value)
    }

    6. Functions

    Let’s define a function:
    // language: go
    func add(a int, b int) int {
      return a + b
    }
    Go can infer types in some cases, but function parameters and return types are usually declared explicitly for clarity.

    We can also return multiple values:
    // language: go
    func divide(a, b float64) (float64, error) {
      if b == 0 {
        return 0, fmt.Errorf("division by zero")
      }
      return a / b, nil
    }
    
    func main() {
      result, err := divide(10, 0)
      if err != nil {
        fmt.Println("Error:", err)
      }
    }
    This (value, error) pattern is very common in Go. Instead of using exceptions, Go encourages explicit error handling, which makes programs more predictable.

    Pass by Value and Reference

    Go passes all arguments by value. However, slices, maps contain internal references, so changes made inside a function can affect the original data. Arrays, on the other hand, are copied entirely, so modifying them doesn’t affect the original.

    // language: go
    func modifyArray(arr [3]int) {
      arr[0] = 99
    }
    
    func modifySlice(slice []int) {
      slice[0] = 99
    }
    
    func main() {
      numsArray := [3]int{1, 2, 3}
      modifyArray(numsArray)
      fmt.Println(numsArray) // [1 2 3] — unchanged
    
      numsSlice := []int{1, 2, 3}
      modifySlice(numsSlice)
      fmt.Println(numsSlice) // [99 2 3] — changed
    }

    7. Modules and Packages

    Every Go file begins with a package declaration. We can organize our code like this:
    myfirstgo/
     ├── main.go
     └── utils/
         └── math.go

    math.go
    // language: go
    package utils
    
    func Square(x int) int {
      return x * x
    }

    main.go
    // language: go
    package main
    
    import (
      "fmt"
      "myfirstgo/utils"
    )
    
    func main() {
      fmt.Println(utils.Square(5))
    }
    Running go run . automatically finds and compiles all packages within the module.

    In Go, visibility (or exporting) is controlled by capitalization, not by keywords like public or private.
    • Exported identifiers (visible outside their package) start with a capital letter.
    • Unexported identifiers (private to the same package) start with a lowercase letter.
    // language: go
    package utils
    
    // Exported: accessible from other packages
    func Add(a, b int) int {
      return a + b
    }
    
    // Unexported: only usable inside the utils package
    func subtract(a, b int) int {
      return a - b
    }

    8. Start Solving a Simple LeetCode Problem with Go

    After reviewing some basic Go syntax, we’ll start with a simple LeetCode problem. It’s easy to forget everything we’ve just learned if we only read about it. We need to write code and use it in practice.

    I started with this problem Two Sum

    Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
    You may assume that each input would have exactly one solution, and you may not use the same element twice.
    You can return the answer in any order.

    Example 1:
    Input: nums = [2,7,11,15], target = 9
    Output: [0,1]
    Explanation: Because nums[0] + nums[1] == 9, we return [0, 1].

    My first draft solution is:
    // language: go
    func twoSum(nums []int, target int) []int {
        // key is number, value is index
        numMap := map[int]int{}
    
        for index, num := range nums {
            rest := target - num
            if numMap[rest] != nil {
                return []int{index, numMap[rest]}
            }
    
            numMap[num] = index
        }
    
        return []int{}
    }

    It causes errors because:
    • numMap[rest] returns an int, so comparing it to nil is invalid (since I'm familiar with JavaScript). Go uses this syntax
    // language: go
    if value, ok := numMap[rest]; ok {
        // do sth
    }
    • Return return {index, restIndex} is invalid, it should be []int{index, restIndex}

    The revised version is:
    // language: go
    func twoSum(nums []int, target int) []int {
        // key is number, value is index
        numMap := make(map[int]int)
    
        for index, num := range nums {
            rest := target - num
            if restIndex, ok := numMap[rest]; ok {
                return []int{index, restIndex}
            }
    
            numMap[num] = index
        }
    
        return []int{}
    }

    I will dive deeper in the next blog, including the error handler and Go routine. See you :D

    Related blogs