Go is a statically typed, compiled language developed by Google in 2009. Designed for simplicity, efficiency, and reliability, Go has become the language of choice for cloud infrastructure, microservices, and distributed systems. This guide will get you from zero to productive in Go in just 60 seconds.
Table of contents
Open Table of contents
Why Go?
Go was created to address the challenges of modern software development at scale. It combines the performance of compiled languages like C++ with the simplicity of interpreted languages like Python. Key advantages include:
- Fast compilation: Go compiles to native machine code incredibly quickly
- Simple syntax: Clean, readable code that’s easy to learn and maintain
- Built-in concurrency: Goroutines and channels make concurrent programming straightforward
- Strong standard library: Comprehensive packages for web development, networking, and more
- Cross-platform: Compile once, run anywhere with native binaries
Basic Syntax
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Variables and Types
Go is statically typed, meaning variable types are determined at compile time. Here are the different ways to declare and initialize variables:
// Explicit variable declaration with type
var name string = "Go"
var age int = 15
var price float64 = 99.99
// Type inference (Go infers the type)
var language = "Go" // string
var version = 1.21 // float64
// Short declaration (most common)
count := 42
isActive := true
message := "Hello, World!"
// Multiple variables
var x, y int = 10, 20
a, b := "hello", "world"
// Zero values (default values for uninitialized variables)
var str string // "" (empty string)
var num int // 0
var flag bool // false
var ptr *int // nil
Common Data Types
// Basic types examples
var (
// Integers
age int = 25 // 4 bytes (32-bit) or 8 bytes (64-bit)
count int64 = 1000000 // 8 bytes
small int8 = 127 // 1 byte
big uint64 = 1844674 // 8 bytes
// Floating point
price float64 = 19.99 // 8 bytes
tax float32 = 0.08 // 4 bytes
// Complex numbers
z1 complex64 = 1 + 2i // 8 bytes
z2 complex128 = 3 + 4i // 16 bytes
// Boolean
isValid bool = true // 1 byte
// String (header size shown, actual string data separate)
name string = "Alice" // 16 bytes header (64-bit)
// Byte and rune
data byte = 255 // 1 byte (alias for uint8)
char rune = 'A' // 4 bytes (alias for int32)
)
📚 Want the complete data types reference? Check out our comprehensive Complete Guide to Go Data Types and Memory Sizes for detailed information about all Go data types, their exact memory sizes, and practical examples.
Conditional Statements (if/else)
age := 18
if age >= 18 {
fmt.Println("Adult")
} else if age >= 13 {
fmt.Println("Teenager")
} else {
fmt.Println("Child")
}
// If with initialization
if num := 10; num > 5 {
fmt.Println("Greater than 5")
}
Functions
Functions in Go are first-class citizens and support multiple return values, which is commonly used for error handling:
// Basic function
func add(a, b int) int {
return a + b
}
// Multiple return values (common pattern for error handling)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// Named return values
func calculate(x, y int) (sum, product int) {
sum = x + y
product = x * y
return // naked return
}
// Variadic functions (variable number of arguments)
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
// Function as a variable
var multiply = func(a, b int) int {
return a * b
}
// Usage examples
func main() {
result := add(5, 3) // 8
quotient, err := divide(10, 2) // 5.0, nil
s, p := calculate(4, 5) // 9, 20
total := sum(1, 2, 3, 4, 5) // 15
product := multiply(6, 7) // 42
}
Arrays and Slices
Arrays have fixed size, while slices are dynamic and more commonly used in Go:
// Arrays (fixed size)
var arr [3]int = [3]int{1, 2, 3}
numbers := [5]int{1, 2, 3, 4, 5}
auto := [...]int{1, 2, 3} // compiler counts elements
// Slices (dynamic arrays - more common)
slice := []int{1, 2, 3, 4}
slice = append(slice, 5) // [1, 2, 3, 4, 5]
slice = append(slice, 6, 7, 8) // [1, 2, 3, 4, 5, 6, 7, 8]
// Creating slices
var empty []int // nil slice
made := make([]int, 5) // [0, 0, 0, 0, 0]
withCap := make([]int, 3, 10) // length 3, capacity 10
// Slicing operations
original := []int{0, 1, 2, 3, 4, 5}
sub := original[1:4] // [1, 2, 3]
prefix := original[:3] // [0, 1, 2]
suffix := original[2:] // [2, 3, 4, 5]
// Common slice operations
fmt.Println(len(slice)) // length
fmt.Println(cap(slice)) // capacity
Maps
Maps are Go’s built-in key-value data structure, similar to dictionaries or hash tables in other languages:
// Map declaration and initialization
ages := map[string]int{
"Alice": 25,
"Bob": 30,
"Charlie": 35,
}
// Creating empty maps
var scores map[string]int // nil map
scores = make(map[string]int) // initialized empty map
grades := make(map[string]int) // alternative syntax
// Adding and accessing values
ages["David"] = 28
age := ages["Alice"] // 25
// Safe access with existence check
age, exists := ages["Eve"]
if exists {
fmt.Printf("Eve is %d years old\n", age)
} else {
fmt.Println("Eve not found")
}
// Deleting entries
delete(ages, "Bob")
// Iterating over maps
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Map length
fmt.Printf("Map has %d entries\n", len(ages))
Structs
Structs are Go’s way to create custom data types and group related data together:
// Defining a struct
type Person struct {
Name string
Age int
Email string
Address Address // embedded struct
}
type Address struct {
Street string
City string
Country string
}
// Creating struct instances
p1 := Person{
Name: "John",
Age: 30,
Email: "john@example.com",
Address: Address{
Street: "123 Main St",
City: "New York",
Country: "USA",
},
}
// Shorthand initialization (field order matters)
p2 := Person{"Alice", 25, "alice@example.com", Address{}}
// Accessing fields
fmt.Println(p1.Name) // John
fmt.Println(p1.Address.City) // New York
// Struct methods
func (p Person) Greet() string {
return fmt.Sprintf("Hello, I'm %s", p.Name)
}
// Pointer receiver (can modify the struct)
func (p *Person) HaveBirthday() {
p.Age++
}
// Usage
fmt.Println(p1.Greet()) // Hello, I'm John
p1.HaveBirthday()
fmt.Println(p1.Age) // 31
Go-Specific Concepts
Goroutines (Concurrency)
// Start a goroutine
go func() {
fmt.Println("Running concurrently")
}()
Channels (Communication)
// Create a channel
ch := make(chan int)
// Send and receive
go func() {
ch <- 42 // Send
}()
value := <-ch // Receive
Defer
func example() {
defer fmt.Println("This runs last")
fmt.Println("This runs first")
}
Error Handling
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
Key Features
- Static typing with type inference
- Garbage collected
- Fast compilation
- Built-in concurrency with goroutines
- No inheritance (composition over inheritance)
- Interface-based polymorphism
Complete Example Project
package main
import (
"fmt"
"time"
)
// Define a struct
type Task struct {
ID int
Name string
Done bool
}
// Method on struct
func (t *Task) Complete() {
t.Done = true
fmt.Printf("Task '%s' completed!\n", t.Name)
}
// Function that uses channels
func processTask(task Task, ch chan<- string) {
// Simulate work
time.Sleep(time.Millisecond * 100)
if task.ID%2 == 0 {
ch <- fmt.Sprintf("Task %d processed successfully", task.ID)
} else {
ch <- fmt.Sprintf("Task %d failed", task.ID)
}
}
func main() {
// Create tasks
tasks := []Task{
{1, "Write code", false},
{2, "Test code", false},
{3, "Deploy code", false},
}
// Channel for communication
results := make(chan string, len(tasks))
// Process tasks concurrently
for _, task := range tasks {
go processTask(task, results)
}
// Collect results
for i := 0; i < len(tasks); i++ {
result := <-results
fmt.Println(result)
}
// Complete first task
tasks[0].Complete()
// Map example
taskStatus := make(map[int]string)
for _, task := range tasks {
if task.Done {
taskStatus[task.ID] = "completed"
} else {
taskStatus[task.ID] = "pending"
}
}
// Print status
fmt.Println("\nTask Status:")
for id, status := range taskStatus {
fmt.Printf("Task %d: %s\n", id, status)
}
// Conditional example
totalTasks := len(tasks)
if totalTasks > 5 {
fmt.Println("Large project")
} else if totalTasks > 2 {
fmt.Println("Medium project")
} else {
fmt.Println("Small project")
}
}
Loops and Control Flow
Go has only one loop construct - the for
loop - but it’s versatile enough to handle all looping scenarios:
// Traditional for loop
for i := 0; i < 5; i++ {
fmt.Println(i)
}
// While-style loop
count := 0
for count < 3 {
fmt.Println("Count:", count)
count++
}
// Infinite loop
for {
// This runs forever (use break to exit)
break
}
// Range loops (iterate over collections)
slice := []string{"apple", "banana", "cherry"}
for index, value := range slice {
fmt.Printf("%d: %s\n", index, value)
}
// Range over map
ages := map[string]int{"Alice": 25, "Bob": 30}
for name, age := range ages {
fmt.Printf("%s is %d\n", name, age)
}
// Ignore index with underscore
for _, value := range slice {
fmt.Println(value)
}
// Switch statements
grade := "A"
switch grade {
case "A":
fmt.Println("Excellent!")
case "B", "C":
fmt.Println("Good job!")
case "D":
fmt.Println("Needs improvement")
default:
fmt.Println("Invalid grade")
}
// Switch without expression (like if-else chain)
score := 85
switch {
case score >= 90:
fmt.Println("A")
case score >= 80:
fmt.Println("B")
case score >= 70:
fmt.Println("C")
default:
fmt.Println("F")
}
Interfaces
Interfaces in Go are implicit - any type that implements all methods of an interface automatically satisfies that interface:
// Define an interface
type Writer interface {
Write([]byte) (int, error)
}
type Speaker interface {
Speak() string
}
// Struct that implements Speaker
type Dog struct {
Name string
}
func (d Dog) Speak() string {
return "Woof!"
}
type Cat struct {
Name string
}
func (c Cat) Speak() string {
return "Meow!"
}
// Function that accepts any Speaker
func makeSound(s Speaker) {
fmt.Println(s.Speak())
}
// Usage
dog := Dog{Name: "Buddy"}
cat := Cat{Name: "Whiskers"}
makeSound(dog) // Woof!
makeSound(cat) // Meow!
// Empty interface can hold any value
var anything interface{}
anything = 42
anything = "hello"
anything = []int{1, 2, 3}
Best Practices
1. Error Handling
Always handle errors explicitly - don’t ignore them:
// Good: explicit error handling
file, err := os.Open("config.txt")
if err != nil {
log.Fatal("Failed to open file:", err)
}
defer file.Close()
// Bad: ignoring errors
file, _ := os.Open("config.txt") // Don't do this!
2. Use defer
for Cleanup
Use defer
to ensure cleanup happens even if errors occur:
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // This will run when function exits
// Process file...
return nil
}
3. Naming Conventions
Follow Go’s naming conventions:
// Exported (public) - starts with capital letter
type User struct {
Name string // exported field
email string // unexported field
}
func NewUser(name, email string) *User { // exported function
return &User{Name: name, email: email}
}
// Unexported (private) - starts with lowercase
func validateEmail(email string) bool {
// validation logic
return true
}
4. Use Short Variable Names
Go encourages short, meaningful variable names:
// Good
for i, user := range users {
fmt.Printf("%d: %s\n", i, user.Name)
}
// Acceptable for longer scope
func processUserData(userData []User) error {
for _, user := range userData {
if err := validateUser(user); err != nil {
return err
}
}
return nil
}
5. Composition over Inheritance
Go doesn’t have inheritance, use composition instead:
type Engine struct {
Power int
}
func (e Engine) Start() {
fmt.Println("Engine starting...")
}
type Car struct {
Engine // embedded struct
Brand string
}
// Car automatically has Start() method
car := Car{
Engine: Engine{Power: 200},
Brand: "Toyota",
}
car.Start() // Works!
References
- Official Go Documentation
- Go by Example
- Effective Go
- Go Tour
- Go Playground
- Awesome Go - Curated list of Go frameworks and libraries