Getting Started with Go: A Practical Beginner’s Guide
Leeting Yan
Go (often called Golang) is a modern programming language designed at Google. It focuses on simplicity, performance, and built-in concurrency. If you want to build fast web services, CLIs, tools, or backend systems, Go is a great choice.
This article will walk you through Go from zero to a small, working example, with plenty of code you can copy, paste, and run.
1. What is Go and Why Use It?
Go is:
- Compiled and statically typed — Fast, with early error detection.
- Simple — A small language spec; easy to read other people’s code.
- Batteries included — Rich standard library (HTTP, JSON, crypto, etc.).
- Great at concurrency — Goroutines and channels are first-class features.
- Cross-platform — Build binaries for Linux, macOS, Windows, etc.
Typical use cases:
- Web services and APIs
- CLI tools and automation
- Microservices and backend systems
- Networking tools and proxies
2. Installing Go
You can check if Go is installed:
go version
You should see something like:
go version go1.24.4 darwin/arm64
If not, download Go from the official website and follow the installer for your OS, then re-open your terminal and check again.
3. Your First Go Program
Let’s write a simple “Hello, Go” program.
Create a folder:
mkdir hello-go
cd hello-go
Initialize a Go module (this sets up dependency management):
go mod init example.com/hello-go
Now create main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, Go!")
}
Run it:
go run .
Or:
go run main.go
You should see:
Hello, Go!
That’s your first Go program.
4. Basic Syntax: Packages, Imports, and main
- Every Go file starts with a package name.
- Executable programs must have package
mainand amain()function. - Use
importto bring in standard library or third-party packages.
Example:
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Current time is:", time.Now())
}
Here:
fmtprovides formatted I/O (Println, Printf, etc.).timeprovides time-related functions.
5. Variables, Types, and Constants
5.1 Variable Declarations
Go is statically typed but supports type inference.
package main
import "fmt"
func main() {
// Explicit type
var age int = 30
// Type inference
var name = "Alice"
// Short declaration (only inside functions)
city := "Berlin"
fmt.Println("Name:", name, "Age:", age, "City:", city)
}
5.2 Basic Types
Common types:
int,int64,float64stringboolrune(Unicode code point)byte(alias foruint8)
a := 10 // int
b := 3.14 // float64
c := "hello" // string
d := true // bool
5.3 Constants
Use const for values that don’t change:
const Pi = 3.14159
const AppName = "MyGoApp"
6. Control Flow: if, for, switch
6.1 if Statements
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("Grade: A")
} else if score >= 80 {
fmt.Println("Grade: B")
} else {
fmt.Println("Grade: C or lower")
}
}
You can also declare variables just for the if:
if length := len("hello"); length > 3 {
fmt.Println("String is longer than 3")
}
6.2 for Loops
Go has only one loop keyword: for.
// Classic for
for i := 0; i < 5; i++ {
fmt.Println("i =", i)
}
// While-style
j := 0
for j < 3 {
fmt.Println("j =", j)
j++
}
// Infinite loop (break manually)
for {
fmt.Println("Looping...")
break
}
6.3 switch
Switches are clean and powerful in Go.
package main
import "fmt"
func main() {
day := "Monday"
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}
}
7. Functions and Multiple Return Values
7.1 Basic Function
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
result := add(10, 20)
fmt.Println("Result:", result)
}
You can shorten parameter types:
func add(a, b int) int {
return a + b
}
7.2 Multiple Return Values
Go functions can return multiple values, often used for results + errors.
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
8. Arrays, Slices, and Maps
8.1 Arrays
Fixed-size sequence of elements.
var nums [3]int
nums[0] = 10
nums[1] = 20
nums[2] = 30
But in Go, we usually prefer slices.
8.2 Slices
Slices are dynamic views over arrays.
package main
import "fmt"
func main() {
// Literal
nums := []int{10, 20, 30}
fmt.Println(nums) // [10 20 30]
// Append
nums = append(nums, 40)
fmt.Println(nums) // [10 20 30 40]
// Range loop
for i, v := range nums {
fmt.Println("Index:", i, "Value:", v)
}
}
8.3 Maps
Maps are key–value dictionaries.
package main
import "fmt"
func main() {
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
ages["Charlie"] = 35
// Access
fmt.Println("Alice:", ages["Alice"])
// Check existence
age, ok := ages["Eve"]
if !ok {
fmt.Println("Eve not found")
} else {
fmt.Println("Eve:", age)
}
// Iterate
for name, age := range ages {
fmt.Println(name, "is", age)
}
}
9. Structs and Methods
9.1 Defining a Struct
Structs group related fields together.
package main
import "fmt"
type User struct {
ID int
Name string
Email string
Age int
}
func main() {
u := User{
ID: 1,
Name: "Alice",
Email: "alice@example.com",
Age: 30,
}
fmt.Printf("User: %+v\n", u)
}
9.2 Methods on Structs
Go doesn’t have classes, but you can define methods on structs.
type User struct {
Name string
Age int
}
// Value receiver (copy)
func (u User) Greet() string {
return fmt.Sprintf("Hi, I'm %s and I'm %d years old.", u.Name, u.Age)
}
// Pointer receiver (can modify the original)
func (u *User) HaveBirthday() {
u.Age++
}
Usage:
func main() {
u := User{Name: "Bob", Age: 20}
fmt.Println(u.Greet())
u.HaveBirthday()
fmt.Println(u.Greet())
}
10. Pointers in Go
Pointers store the address of a value.
package main
import "fmt"
func increment(num *int) {
*num = *num + 1
}
func main() {
value := 10
fmt.Println("Before:", value)
increment(&value)
fmt.Println("After:", value)
}
Notes:
&valuegives the address ofvalue.*numdereferences the pointer to read/write the actual value.- Go doesn’t support pointer arithmetic (unlike C).
11. Interfaces: Behavior, Not Inheritance
Interfaces let you define behavior without specifying how it’s implemented.
package main
import "fmt"
// Interface
type Notifier interface {
Notify(message string)
}
// Struct 1
type EmailNotifier struct {
Email string
}
func (e EmailNotifier) Notify(message string) {
fmt.Println("Sending EMAIL to", e.Email+":", message)
}
// Struct 2
type SMSNotifier struct {
Phone string
}
func (s SMSNotifier) Notify(message string) {
fmt.Println("Sending SMS to", s.Phone+":", message)
}
func sendAlert(n Notifier, message string) {
n.Notify(message)
}
func main() {
email := EmailNotifier{Email: "user@example.com"}
sms := SMSNotifier{Phone: "+123456789"}
sendAlert(email, "Welcome to Go!")
sendAlert(sms, "Your code just compiled successfully.")
}
Any type that has a Notify(string) method implements Notifier automatically. No implements keyword needed.
12. Organizing Code with Packages and Modules
Go encourages clear, simple package structures.
Example project:
myapp/
go.mod
main.go
user/
user.go
user/user.go:
package user
type User struct {
ID int
Name string
}
func New(id int, name string) User {
return User{
ID: id,
Name: name,
}
}
main.go:
package main
import (
"fmt"
"example.com/myapp/user"
)
func main() {
u := user.New(1, "Alice")
fmt.Printf("User: %+v\n", u)
}
Your go.mod should have the module path:
module example.com/myapp
You can choose any module path, but it’s common to use a Git repo URL.
13. Concurrency Basics: Goroutines and Channels
One of Go’s biggest strengths is concurrency.
13.1 Goroutines
A goroutine is a lightweight thread managed by Go’s runtime.
package main
import (
"fmt"
"time"
)
func printNumbers(prefix string) {
for i := 1; i <= 3; i++ {
fmt.Println(prefix, i)
time.Sleep(200 * time.Millisecond)
}
}
func main() {
go printNumbers("A") // run concurrently
go printNumbers("B") // run concurrently
// Keep main alive long enough to see output
time.Sleep(1 * time.Second)
fmt.Println("Done")
}
13.2 Channels
Channels are typed pipes you use to send values between goroutines.
package main
import (
"fmt"
"time"
)
func worker(jobs <-chan int, results chan<- int) {
for job := range jobs {
// Simulate work
time.Sleep(100 * time.Millisecond)
results <- job * 2
}
}
func main() {
jobs := make(chan int, 5)
results := make(chan int, 5)
// Start 2 workers
go worker(jobs, results)
go worker(jobs, results)
// Send jobs and close the channel
for i := 1; i <= 5; i++ {
jobs <- i
}
close(jobs)
// Collect results
for i := 1; i <= 5; i++ {
res := <-results
fmt.Println("Result:", res)
}
}
Notes:
jobs <- 1sends a value into the channel.<-jobsreceives a value.jobs <-chan intmeans the channel is receive-only for that function.results chan<- intmeans send-only.
14. A Tiny HTTP API in Go
Let’s build a very small HTTP server to see how Go works in a realistic scenario.
Project structure:
mini-api/
go.mod
main.go
Initialize:
mkdir mini-api
cd mini-api
go mod init example.com/mini-api
main.go:
package main
import (
"encoding/json"
"log"
"net/http"
"time"
)
// Response is a simple JSON structure
type Response struct {
Message string `json:"message"`
Timestamp time.Time `json:"timestamp"`
}
func main() {
// Handle /hello route
http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
resp := Response{
Message: "Hello from Go HTTP server!",
Timestamp: time.Now(),
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(resp); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
// Simple health check
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
addr := ":8080"
log.Println("Server listening on", addr)
if err := http.ListenAndServe(addr, nil); err != nil {
log.Fatal("Server error:", err)
}
}
Run:
go run .
Open in your browser:
http://localhost:8080/hellohttp://localhost:8080/healthz
You’ll see JSON output for /hello and plain text ok for /healthz.
15. Building and Distributing Your Program
Once your code works, build a binary:
go build -o mini-api .
This produces an executable (mini-api or mini-api.exe on Windows). You can copy this binary to another machine with the same OS/architecture and run it directly.
You can also cross-compile by setting GOOS and GOARCH:
GOOS=linux GOARCH=amd64 go build -o mini-api-linux .
16. Where to Go Next
You now know:
- How to write and run basic Go programs
- Variables, control flow, functions
- Slices, maps, structs, methods, interfaces
- Basics of concurrency with goroutines and channels
- How to build a small HTTP API
Next steps:
- Explore the standard library (
net/http,io,os,context,sync,time). - Learn about testing with
go test. - Try building a CLI tool or a more complex web service.
- Read Go’s official “Effective Go” and Go blog posts.