Golang Basics for Beginners
Getting started with Go programming language? Here are the fundamental concepts you need to know.
Getting started with Go programming language? Here are the fundamental concepts you need to know.

Introduction: In a world where software needs to be fast, efficient, and easy to maintain, Go (also known as Golang) has emerged as a powerful solution. Created by Google engineers in 2009, Go was designed to address common frustrations in software development: slow compilation times, complex dependency management, and the difficulty of writing concurrent programs. Whether you're building web servers, command-line tools, or cloud services, Go provides a clean, efficient way to get things done.
Go is a statically typed, compiled programming language that combines the performance of lower-level languages like C with the ease of use found in modern languages like Python. It's known for its simplicity, excellent concurrency support, and fast compilation times, making it a favorite among developers building scalable backend services and cloud infrastructure.
Before diving into syntax and code, let's understand what makes Go special and why it's worth learning.
Go was designed with simplicity as a core principle. The language has a small number of keywords (only 25) and a straightforward syntax that's easy to learn and read. There's usually one obvious way to do things, which makes Go code remarkably consistent across different projects and teams.
This simplicity doesn't mean Go is limited. Instead, it means you can focus on solving problems rather than wrestling with language complexity. You can become productive in Go much faster than in many other languages.
As a compiled language, Go produces fast, efficient binaries. Go programs typically run at speeds comparable to C or C++, making it suitable for performance-critical applications. The Go compiler is also incredibly fast, allowing for rapid development cycles.
Go's goroutines and channels make concurrent programming straightforward and accessible. While concurrency is notoriously difficult in many languages, Go's design makes it natural and easy to write programs that do multiple things at once efficiently.
Go comes with a comprehensive standard library that handles everything from HTTP servers to JSON parsing, cryptography, and file I/O. You can build complete applications using just the standard library, reducing external dependencies.
Go compiles incredibly quickly, even for large codebases. This means you get almost instant feedback when developing, similar to interpreted languages but with the performance of compiled code.
Go makes it easy to build applications for different operating systems and architectures. With simple build commands, you can create binaries for Windows, macOS, Linux, and more from a single codebase.
Go powers critical infrastructure at companies like Google, Uber, Netflix, and Dropbox. Popular projects like Docker, Kubernetes, and Terraform are written in Go. Learning Go opens doors to working on cutting-edge cloud and infrastructure projects.
Let's get Go installed and configured on your system.
macOS:
# Using Homebrew
brew install go
# Verify installation
go version
Linux:
# Download and extract
wget https://go.dev/dl/go1.21.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz
# Add to PATH (add to ~/.bashrc or ~/.zshrc)
export PATH=$PATH:/usr/local/go/bin
# Verify installation
go version
Windows:
Download the installer from https://go.dev/dl/ and run it. The installer will set up Go and add it to your PATH automatically.
Go uses a workspace directory to organize your code. By default, this is ~/go.
# View Go environment variables
go env
# Important variables:
# GOPATH - your workspace directory
# GOROOT - where Go is installed
# GOBIN - where compiled binaries go
# Create a project directory
mkdir hello-go
cd hello-go
# Initialize a Go module
go mod init example.com/hello
# This creates a go.mod file that tracks dependencies
Popular editors for Go development:
VS Code:
GoLand:
Vim/Neovim:
Let's write and understand a classic "Hello, World!" program in Go.
Create a file named main.go:
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
Package Declaration:
package main
Every Go file starts with a package declaration. The main package is special—it defines an executable program rather than a library. When you build a main package, Go creates an executable binary.
Import Statement:
import "fmt"
The import keyword brings in other packages. Here we import fmt (format), which provides formatted I/O functions. The standard library has many useful packages like net/http, encoding/json, and database/sql.
Multiple imports:
import (
"fmt"
"time"
"math"
)
Main Function:
func main() {
fmt.Println("Hello, World!")
}
The main function is the entry point of the program. When you run a Go program, execution starts here. Every executable Go program must have exactly one main function in the main package.
# Run directly (compiles and executes)
go run main.go
# Build an executable
go build main.go
# Run the executable
./main
package main
import "fmt"
// This is a single-line comment
/*
This is a
multi-line comment
*/
// main is the entry point of the program
func main() {
// Print a greeting message
fmt.Println("Hello, World!")
}
Go is statically typed, meaning every variable has a specific type that must be known at compile time.
Go has several built-in types:
Boolean:
var isActive bool = true
var isComplete bool = false
Numeric Types:
// Integers
var age int = 25 // Platform-dependent size
var count int8 = 127 // -128 to 127
var bigNumber int64 = 1000000 // -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807
// Unsigned integers
var positive uint = 42 // Only positive numbers
var byte uint8 = 255 // 0 to 255
// Floating point
var price float32 = 19.99
var precise float64 = 3.14159265359
// Complex numbers
var complex complex64 = 1 + 2i
Strings:
var name string = "John Doe"
var message string = `This is a
multi-line string
using backticks`
Go offers multiple ways to declare variables:
Explicit Type Declaration:
var name string = "Alice"
var age int = 30
var isStudent bool = false
Type Inference:
var name = "Alice" // Go infers string type
var age = 30 // Go infers int type
var price = 19.99 // Go infers float64 type
Short Variable Declaration (inside functions only):
func main() {
name := "Alice" // Shorthand, type inferred
age := 30
price := 19.99
// Multiple variables
x, y := 10, 20
firstName, lastName := "John", "Doe"
}
Declaring Multiple Variables:
var (
name string = "Alice"
age int = 30
salary float64 = 50000.00
)
Constants are immutable values known at compile time:
const Pi = 3.14159
const AppName = "MyApp"
const MaxRetries = 3
// Multiple constants
const (
StatusOK = 200
StatusNotFound = 404
StatusError = 500
)
// iota for enumerated constants
const (
Monday = iota // 0
Tuesday // 1
Wednesday // 2
Thursday // 3
Friday // 4
Saturday // 5
Sunday // 6
)
In Go, variables declared without an explicit initial value are given their zero value:
var i int // 0
var f float64 // 0.0
var b bool // false
var s string // "" (empty string)
var p *int // nil
Go requires explicit type conversion:
var x int = 42
var y float64 = float64(x) // Convert int to float64
var z uint = uint(x) // Convert int to uint
// String conversions
import "strconv"
// Int to string
numStr := strconv.Itoa(42) // "42"
// String to int
num, err := strconv.Atoi("42") // 42, nil
if err != nil {
// Handle error
}
// Float to string
floatStr := strconv.FormatFloat(3.14, 'f', 2, 64) // "3.14"
package main
import "fmt"
func main() {
// Different ways to declare variables
var name string = "Alice"
var age = 30
salary := 75000.50
// Constants
const company = "TechCorp"
// Multiple variables
firstName, lastName := "John", "Doe"
// Zero values
var count int
var active bool
fmt.Println("Name:", name)
fmt.Println("Age:", age)
fmt.Println("Salary:", salary)
fmt.Println("Company:", company)
fmt.Printf("%s %s\n", firstName, lastName)
fmt.Println("Count (zero value):", count)
fmt.Println("Active (zero value):", active)
}
Go has straightforward control structures for decision-making and loops.
// Basic if
if age >= 18 {
fmt.Println("Adult")
}
// If-else
if temperature > 30 {
fmt.Println("Hot")
} else {
fmt.Println("Not hot")
}
// If-else if-else
if score >= 90 {
fmt.Println("A")
} else if score >= 80 {
fmt.Println("B")
} else if score >= 70 {
fmt.Println("C")
} else {
fmt.Println("F")
}
// If with initialization statement
if age := getAge(); age >= 18 {
fmt.Println("Adult, age:", age)
} else {
fmt.Println("Minor, age:", age)
}
// age is only available inside the if block
// Basic switch
day := "Monday"
switch day {
case "Monday":
fmt.Println("Start of work week")
case "Friday":
fmt.Println("End of work week")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Midweek")
}
// Switch with expressions
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")
}
// Switch with type
var i interface{} = "hello"
switch v := i.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
default:
fmt.Println("Unknown type")
}
Go has only one loop construct: for. But it's flexible enough to handle all loop scenarios.
Traditional for loop:
for i := 0; i < 5; i++ {
fmt.Println(i)
}
While-style loop:
count := 0
for count < 5 {
fmt.Println(count)
count++
}
Infinite loop:
for {
// Loop forever (until break)
if condition {
break
}
}
Range loop (iterate over collections):
// Array/Slice
numbers := []int{1, 2, 3, 4, 5}
for index, value := range numbers {
fmt.Printf("Index: %d, Value: %d\n", index, value)
}
// Just values
for _, value := range numbers {
fmt.Println(value)
}
// Just indices
for index := range numbers {
fmt.Println(index)
}
// String (iterates over runes)
for index, char := range "Hello" {
fmt.Printf("%d: %c\n", index, char)
}
// Map
ages := map[string]int{"Alice": 30, "Bob": 25}
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Break - exit loop
for i := 0; i < 10; i++ {
if i == 5 {
break
}
fmt.Println(i)
}
// Continue - skip to next iteration
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i) // Only prints odd numbers
}
// Labeled breaks (for nested loops)
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer
}
fmt.Printf("i=%d, j=%d\n", i, j)
}
}
Functions are the building blocks of Go programs. They help you organize code and make it reusable.
// Function with parameters and return value
func add(x int, y int) int {
return x + y
}
// Shortened parameter syntax (same type)
func multiply(x, y int) int {
return x * y
}
// Function with no parameters
func sayHello() {
fmt.Println("Hello!")
}
// Function with no return value
func printSum(a, b int) {
fmt.Println(a + b)
}
Go functions can return multiple values, commonly used for returning results and errors:
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Using the function
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
// Ignoring return values with _
result, _ := divide(10, 2) // Ignore error
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // Naked return (returns x and y)
}
// Usage
a, b := split(17)
fmt.Println(a, b) // 7, 10
Functions that accept a variable number of arguments:
func sum(numbers ...int) int {
total := 0
for _, num := range numbers {
total += num
}
return total
}
// Usage
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Pass a slice
nums := []int{1, 2, 3, 4}
fmt.Println(sum(nums...)) // 10
// Anonymous function
func() {
fmt.Println("Anonymous function")
}()
// Assigned to variable
add := func(x, y int) int {
return x + y
}
fmt.Println(add(3, 5)) // 8
// Closure
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
c := counter()
fmt.Println(c()) // 1
fmt.Println(c()) // 2
fmt.Println(c()) // 3
The defer statement schedules a function call to be executed after the surrounding function returns:
func main() {
defer fmt.Println("World")
fmt.Println("Hello")
}
// Output:
// Hello
// World
// Multiple defers (LIFO - Last In First Out)
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
}
// Output:
// 3
// 2
// 1
// Common use: closing resources
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // Ensures file is closed
// Read file...
return nil
}
Arrays and slices are fundamental data structures in Go for storing collections of elements.
Arrays have a fixed size determined at compile time:
// Declare array
var numbers [5]int // Array of 5 integers, initialized to [0 0 0 0 0]
// Initialize with values
primes := [5]int{2, 3, 5, 7, 11}
// Let Go count the elements
primes := [...]int{2, 3, 5, 7, 11} // Size is 5
// Access elements
fmt.Println(primes[0]) // 2
primes[1] = 13
fmt.Println(primes) // [2 13 5 7 11]
// Array length
fmt.Println(len(primes)) // 5
// Iterate over array
for i, v := range primes {
fmt.Printf("Index %d: %d\n", i, v)
}
Slices are more flexible than arrays—they can grow and shrink:
// Create a slice
var numbers []int // Empty slice
// Slice literal
primes := []int{2, 3, 5, 7, 11}
// Create slice with make
scores := make([]int, 5) // Length 5, capacity 5, initialized to [0 0 0 0 0]
scores := make([]int, 3, 10) // Length 3, capacity 10
// Length and capacity
fmt.Println(len(scores)) // 3
fmt.Println(cap(scores)) // 10
// Append elements
numbers := []int{1, 2, 3}
numbers = append(numbers, 4)
numbers = append(numbers, 5, 6, 7)
fmt.Println(numbers) // [1 2 3 4 5 6 7]
// Append another slice
more := []int{8, 9, 10}
numbers = append(numbers, more...)
fmt.Println(numbers) // [1 2 3 4 5 6 7 8 9 10]
// Slicing
nums := []int{0, 1, 2, 3, 4, 5}
slice1 := nums[1:4] // [1 2 3]
slice2 := nums[:3] // [0 1 2]
slice3 := nums[3:] // [3 4 5]
slice4 := nums[:] // [0 1 2 3 4 5] (full slice)
// Copy slice
src := []int{1, 2, 3}
dst := make([]int, len(src))
copy(dst, src)
fmt.Println(dst) // [1 2 3]
Slices are references to underlying arrays. Modifying a slice can affect other slices:
original := []int{1, 2, 3, 4, 5}
slice1 := original[1:3] // [2 3]
slice2 := original[2:4] // [3 4]
slice1[1] = 99
fmt.Println(original) // [1 2 99 4 5]
fmt.Println(slice1) // [2 99]
fmt.Println(slice2) // [99 4]
// 2D slice (matrix)
matrix := [][]int{
{1, 2, 3},
{4, 5, 6},
{7, 8, 9},
}
fmt.Println(matrix[1][2]) // 6
// Dynamic 2D slice
rows := 3
cols := 4
matrix := make([][]int, rows)
for i := range matrix {
matrix[i] = make([]int, cols)
}
Maps are Go's built-in hash table or dictionary type. They store key-value pairs.
// Declare a map
var ages map[string]int // nil map, can't add elements
// Initialize with make
ages := make(map[string]int)
// Map literal
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 35,
}
// Alternative syntax
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
// Add/Update elements
ages["David"] = 28
ages["Alice"] = 31 // Update existing
// Get value
age := ages["Alice"]
fmt.Println(age) // 31
// Check if key exists
age, exists := ages["Eve"]
if exists {
fmt.Println("Eve's age:", age)
} else {
fmt.Println("Eve not found")
}
// Delete element
delete(ages, "Bob")
// Get length
fmt.Println(len(ages))
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Carol": 35,
}
// Iterate over map (order is random)
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Just keys
for name := range ages {
fmt.Println(name)
}
// Just values
for _, age := range ages {
fmt.Println(age)
}
Since maps return zero values for missing keys, you can use them as sets:
// Set of strings
visited := make(map[string]bool)
// Add to set
visited["page1"] = true
visited["page2"] = true
// Check membership
if visited["page1"] {
fmt.Println("Already visited page1")
}
// Alternative: using empty struct (more memory efficient)
visited := make(map[string]struct{})
visited["page1"] = struct{}{}
if _, exists := visited["page1"]; exists {
fmt.Println("Already visited")
}
Structs allow you to create custom types by grouping related data together.
// Basic struct
type Person struct {
FirstName string
LastName string
Age int
}
// Struct with embedded types
type Address struct {
Street string
City string
ZipCode string
}
type Employee struct {
Person // Embedded struct
ID int
Department string
Address Address
}
// Create struct with field names
person1 := Person{
FirstName: "John",
LastName: "Doe",
Age: 30,
}
// Create struct with positional values (not recommended)
person2 := Person{"Jane", "Smith", 25}
// Zero value struct
var person3 Person // All fields are zero values
// Access fields
fmt.Println(person1.FirstName) // John
person1.Age = 31
fmt.Println(person1.Age) // 31
// Pointer to struct
p := &Person{"Alice", "Johnson", 28}
fmt.Println(p.FirstName) // Alice (Go auto-dereferences)
Methods are functions with a receiver argument:
type Person struct {
FirstName string
LastName string
}
// Method with value receiver
func (p Person) FullName() string {
return p.FirstName + " " + p.LastName
}
// Method with pointer receiver (can modify the struct)
func (p *Person) UpdateName(first, last string) {
p.FirstName = first
p.LastName = last
}
// Usage
person := Person{"John", "Doe"}
fmt.Println(person.FullName()) // John Doe
person.UpdateName("Jane", "Smith")
fmt.Println(person.FullName()) // Jane Smith
Use pointer receivers when:
type Circle struct {
Radius float64
}
// Value receiver - doesn't modify original
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
// Pointer receiver - modifies original
func (c *Circle) Scale(factor float64) {
c.Radius *= factor
}
// Usage
circle := Circle{Radius: 5}
fmt.Println(circle.Area()) // 78.53975
circle.Scale(2)
fmt.Println(circle.Radius) // 10
Struct tags provide metadata for struct fields, commonly used for JSON encoding/decoding:
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
Password string `json:"-"` // Never include in JSON
}
// JSON encoding
user := User{
ID: 1,
Username: "johndoe",
Email: "john@example.com",
Password: "secret",
}
jsonData, _ := json.Marshal(user)
fmt.Println(string(jsonData))
// {"id":1,"username":"johndoe","email":"john@example.com"}
Pointers store the memory address of a value. They're essential for efficient data manipulation and understanding how Go manages memory.
// Declare a variable
x := 42
// Create a pointer to x
var p *int = &x // & gets the address
fmt.Println(p) // 0xc000... (memory address)
fmt.Println(*p) // 42 (* dereferences, gets the value)
// Modify through pointer
*p = 21
fmt.Println(x) // 21 (x is changed)
// Zero value of pointer is nil
var ptr *int
fmt.Println(ptr) // <nil>
// Pass by value (doesn't modify original)
func increment(x int) {
x++
}
// Pass by pointer (modifies original)
func incrementByPointer(x *int) {
*x++
}
// Usage
num := 5
increment(num)
fmt.Println(num) // 5 (unchanged)
incrementByPointer(&num)
fmt.Println(num) // 6 (changed)
Efficiency: Avoid copying large structures:
type LargeStruct struct {
data [1000000]int
}
// Inefficient - copies entire struct
func processBad(s LargeStruct) {
// Process data
}
// Efficient - passes pointer
func processGood(s *LargeStruct) {
// Process data
}
Mutability: Modify data in place:
type Counter struct {
value int
}
func (c *Counter) Increment() {
c.value++
}
counter := Counter{}
counter.Increment()
fmt.Println(counter.value) // 1
The new function allocates memory and returns a pointer:
// Using new
p := new(int)
*p = 42
fmt.Println(*p) // 42
// Equivalent to:
var i int
p := &i
*p = 42
Interfaces define behavior—a set of method signatures. They're one of Go's most powerful features, enabling polymorphism and flexible code design.
// Interface definition
type Shape interface {
Area() float64
Perimeter() float64
}
// Implementing the interface (implicit)
type Rectangle struct {
Width, Height float64
}
func (r Rectangle) Area() float64 {
return r.Width * r.Height
}
func (r Rectangle) Perimeter() float64 {
return 2 * (r.Width + r.Height)
}
type Circle struct {
Radius float64
}
func (c Circle) Area() float64 {
return 3.14159 * c.Radius * c.Radius
}
func (c Circle) Perimeter() float64 {
return 2 * 3.14159 * c.Radius
}
func printShapeInfo(s Shape) {
fmt.Printf("Area: %.2f\n", s.Area())
fmt.Printf("Perimeter: %.2f\n", s.Perimeter())
}
// Usage
rect := Rectangle{Width: 10, Height: 5}
circle := Circle{Radius: 7}
printShapeInfo(rect) // Works!
printShapeInfo(circle) // Works!
The empty interface interface{} can hold values of any type:
func describe(i interface{}) {
fmt.Printf("Type: %T, Value: %v\n", i, i)
}
describe(42)
describe("hello")
describe(3.14)
describe([]int{1, 2, 3})
Extract the underlying concrete type from an interface:
var i interface{} = "hello"
// Type assertion
s := i.(string)
fmt.Println(s) // hello
// Type assertion with check
s, ok := i.(string)
if ok {
fmt.Println("String:", s)
}
f, ok := i.(float64)
if !ok {
fmt.Println("Not a float64")
}
func classify(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %s\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}
classify(42)
classify("hello")
classify(true)
Stringer: Custom string representation:
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years)", p.Name, p.Age)
}
person := Person{"Alice", 30}
fmt.Println(person) // Alice (30 years)
Error: Error handling:
type error interface {
Error() string
}
// Custom error
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
Go uses explicit error handling rather than exceptions. Errors are values that can be checked and handled.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Usage
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
import "fmt"
// Using fmt.Errorf
func validateAge(age int) error {
if age < 0 {
return fmt.Errorf("age cannot be negative: %d", age)
}
if age > 150 {
return fmt.Errorf("age seems invalid: %d", age)
}
return nil
}
// Custom error type
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s (%v): %s", e.Field, e.Value, e.Message)
}
func validateUser(name string, age int) error {
if name == "" {
return &ValidationError{
Field: "name",
Value: name,
Message: "cannot be empty",
}
}
if age < 18 {
return &ValidationError{
Field: "age",
Value: age,
Message: "must be 18 or older",
}
}
return nil
}
import "fmt"
func readConfig(filename string) error {
err := openFile(filename)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
return nil
}
// Check wrapped errors
err := readConfig("config.json")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file doesn't exist")
}
// Extract specific error type
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Path error:", pathErr.Path)
}
For truly exceptional situations:
// Panic stops normal execution
func mustReadFile(filename string) string {
data, err := os.ReadFile(filename)
if err != nil {
panic(err) // Panic if critical error
}
return string(data)
}
// Recover from panic
func safeExecute() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from panic:", r)
}
}()
// Code that might panic
mustReadFile("nonexistent.txt")
fmt.Println("This won't execute if panic occurs")
}
Goroutines are lightweight threads managed by the Go runtime. They make concurrent programming simple and efficient.
import "time"
func sayHello() {
fmt.Println("Hello")
}
func main() {
// Run function in a goroutine
go sayHello()
// Run anonymous function
go func() {
fmt.Println("World")
}()
// Wait for goroutines (naive approach)
time.Sleep(time.Second)
}
Channels are the pipes that connect concurrent goroutines:
// Create a channel
ch := make(chan int)
// Send value to channel (blocks until received)
go func() {
ch <- 42
}()
// Receive value from channel (blocks until sent)
value := <-ch
fmt.Println(value) // 42
// Create buffered channel (holds 3 values)
ch := make(chan int, 3)
// Send without blocking (until buffer is full)
ch <- 1
ch <- 2
ch <- 3
// Receive
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
fmt.Println(<-ch) // 3
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c) // Close channel when done
}
// Receive values until channel is closed
ch := make(chan int, 10)
go fibonacci(10, ch)
for num := range ch {
fmt.Println(num)
}
Choose from multiple channel operations:
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "from channel 1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "from channel 2"
}()
for i := 0; i < 2; i++ {
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
case <-time.After(3 * time.Second):
fmt.Println("timeout")
}
}
}
Properly wait for goroutines to finish:
import "sync"
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // Decrement counter when done
fmt.Printf("Worker %d starting\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d done\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1) // Increment counter
go worker(i, &wg)
}
wg.Wait() // Wait for all workers
fmt.Println("All workers done")
}
Protect shared data from concurrent access:
import "sync"
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func main() {
counter := SafeCounter{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
fmt.Println(counter.Value()) // 1000
}
Go uses packages to organize code and modules to manage dependencies.
// File: mathutil/mathutil.go
package mathutil
// Exported function (starts with capital letter)
func Add(a, b int) int {
return a + b
}
// Unexported function (starts with lowercase)
func subtract(a, b int) int {
return a - b
}
// File: main.go
package main
import (
"fmt"
"mathutil" // Your package
)
func main() {
result := mathutil.Add(3, 5)
fmt.Println(result) // 8
// mathutil.subtract(5, 3) // Error: unexported
}
Initialize a module:
go mod init example.com/myproject
Add dependencies:
# Install and add to go.mod
go get github.com/gin-gonic/gin
The go.mod file tracks dependencies:
module example.com/myproject
go 1.21
require github.com/gin-gonic/gin v1.9.1
import (
"fmt" // Standard library
"example.com/myproject/util" // Local package
"github.com/gin-gonic/gin" // External package
)
Use meaningful package names: Short, clear, lowercase names
One concept per package: Each package should have a single, well-defined purpose
Keep packages small: Better to have many small packages than few large ones
Variables and functions: camelCase for unexported, PascalCase for exported
// Good
var userCount int
func getUserByID(id int) User
// Exported
var MaxConnections int
func CreateUser(name string) User
Interfaces: Often end in "-er" (Reader, Writer, Closer)
Avoid stuttering: Don't repeat package name in names
// Bad
package user
type UserService struct {}
// Good
package user
type Service struct {}
Check errors: Never ignore errors
// Bad
result, _ := doSomething()
// Good
result, err := doSomething()
if err != nil {
return err
}
Return errors, don't panic: Panics are for truly exceptional situations
Provide context: Wrap errors with helpful messages
if err != nil {
return fmt.Errorf("failed to create user: %w", err)
}
Document exported items:
// Add returns the sum of two integers.
// It performs basic integer addition.
func Add(a, b int) int {
return a + b
}
Package documentation:
// Package mathutil provides basic mathematical operations.
package mathutil
Write tests for your code:
// File: mathutil_test.go
package mathutil
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
}
Run tests:
go test
go test -v # Verbose output
go test -cover # Show coverage
You've now learned the fundamentals of Go programming. From basic types and control structures to advanced concepts like goroutines and interfaces, you have the foundation needed to build real-world applications.
Go's simplicity doesn't mean it's limited—major companies rely on Go for critical infrastructure. The language's focus on clarity, performance, and built-in concurrency support makes it an excellent choice for modern software development.
Remember, mastering any programming language takes practice. Write code every day, experiment with different approaches, and don't be afraid to make mistakes. The Go community is welcoming and helpful, so don't hesitate to ask questions and share your learning journey.
Happy coding in Go!