general

Go for Python Developers: A Practical Guide to Google's Language

A hands-on guide for Python developers transitioning to Go. Covers syntax differences, concurrency patterns, type system, and real-world examples with side-by-side comparisons.

Ioodu · · 20 min read
#go #golang #python #programming #backend #learning #language-comparison

Go for Python Developers: A Practical Guide to Google’s Language

You’ve mastered Python. You can spin up a web API in Flask in 10 minutes, process data with Pandas like a pro, and write clean, Pythonic code. But now you’re looking at Go—and it feels alien.

The good news: Your Python experience is a solid foundation. The patterns you know (simplicity, readability, batteries included) translate well. The challenge: Go does things differently, and trying to write Go like Python will frustrate you.

This guide bridges the gap. We’ll compare Python and Go side-by-side, show you where the languages differ philosophically, and get you writing idiomatic Go.

Why Go in 2026?

Go was designed at Google to solve specific problems:

  • Fast compilation: No more waiting minutes for builds
  • Simple deployment: Single binary, no dependencies
  • Efficient concurrency: Goroutines vs. threads
  • Clear, readable code: Explicit over implicit

In 2026, Go dominates:

  • Cloud-native tools: Docker, Kubernetes, Terraform
  • Backend services: High-performance APIs
  • DevOps/CLI tools: Fast, portable utilities
  • Data infrastructure: Kafka, ClickHouse, InfluxDB

If you’re building infrastructure, cloud services, or performance-critical backends, Go is often the right choice.


Go vs Python: Philosophy

Python’s Philosophy

# "There's one way to do it" (but really, many ways)
# Readability counts
# Explicit is better than implicit (except when it's not)

# Multiple ways to do the same thing
def get_name(obj):
    return obj.name

get_name = lambda obj: obj.name

class NameGetter:
    def __call__(self, obj):
        return obj.name

get_name = operator.attrgetter('name')

Go’s Philosophy

// "There is exactly one way to do it"
// Simplicity is paramount
// Explicit is the only option

// One way to get a field
func getName(obj Person) string {
    return obj.Name
}

// That's it. No lambdas, no operator overloading, no magic.

Key Difference: Python gives you flexibility. Go gives you consistency. Neither is wrong, but they optimize for different things.


Syntax Comparison

Variables

# Python - dynamic typing
name = "Alice"           # str
age = 30                 # int
is_active = True         # bool
salary = 75000.50        # float
names = ["Alice", "Bob"] # list
user = {"name": "Alice"} # dict

# Type hints (optional)
from typing import List, Dict

def greet(name: str) -> str:
    return f"Hello, {name}"
// Go - static typing
name := "Alice"              // string
age := 30                    // int
isActive := true             // bool
salary := 75000.50           // float64
names := []string{"Alice", "Bob"}  // slice
user := map[string]string{"name": "Alice"} // map

// Explicit types
var count int = 10
var message string = "Hello"

// Functions with types
func greet(name string) string {
    return "Hello, " + name
}

Key Differences:

  • Go requires types (or := for inference)
  • No var keyword in Python; Go uses var or :=
  • Go uses PascalCase for exported names, camelCase for private

Conditionals

# Python
if age >= 18 and age < 65:
    print("Working age")
elif age >= 65:
    print("Retirement age")
else:
    print("Too young")

# Pythonic truthiness
if user_list:  # True if not empty
    print("Has users")
// Go
if age >= 18 && age < 65 {
    fmt.Println("Working age")
} else if age >= 65 {
    fmt.Println("Retirement age")
} else {
    fmt.Println("Too young")
}

// No implicit truthiness - must be explicit
if len(userList) > 0 {
    fmt.Println("Has users")
}

// Error handling is explicit
file, err := os.Open("data.txt")
if err != nil {
    log.Fatal(err)
}
// Must check err before using file

Key Differences:

  • Go uses && not and, || not or
  • No truthiness—must check length/nil explicitly
  • Error handling is mandatory, not exceptions

Loops

# Python - many options
# For loop
for i in range(5):
    print(i)

# For each
for name in names:
    print(name)

# While
while count > 0:
    count -= 1

# List comprehension
upper_names = [n.upper() for n in names if len(n) > 3]
// Go - only one loop construct
// For loop
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// For each (range over slice)
for _, name := range names {
    fmt.Println(name)
}

// While
for count > 0 {
    count--
}

// No comprehensions - must use explicit loops
var upperNames []string
for _, n := range names {
    if len(n) > 3 {
        upperNames = append(upperNames, strings.ToUpper(n))
    }
}

Key Difference: Go has one loop. Python has many. Go is more verbose but explicit.


Type System Deep Dive

Structs vs Classes

# Python - class with methods
class Person:
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def greet(self) -> str:
        return f"Hi, I'm {self.name}"

    def is_adult(self) -> bool:
        return self.age >= 18

alice = Person("Alice", 30)
print(alice.greet())
// Go - struct with methods
type Person struct {
    Name string
    Age  int
}

// Method with value receiver (immutable)
func (p Person) Greet() string {
    return fmt.Sprintf("Hi, I'm %s", p.Name)
}

// Method with pointer receiver (mutable)
func (p *Person) HaveBirthday() {
    p.Age++
}

// Function (not method)
func IsAdult(p Person) bool {
    return p.Age >= 18
}

alice := Person{Name: "Alice", Age: 30}
fmt.Println(alice.Greet())

Key Differences:

  • Go has no classes, only structs
  • No inheritance (use composition)
  • Methods are defined outside the struct
  • Pointer vs value receivers matter

Composition over Inheritance

# Python - inheritance
class Employee(Person):
    def __init__(self, name: str, age: int, salary: float):
        super().__init__(name, age)
        self.salary = salary

    def bonus(self) -> float:
        return self.salary * 0.1
// Go - composition
type Employee struct {
    Person   // Embedded struct (like inheritance but explicit)
    Salary   float64
}

func (e Employee) Bonus() float64 {
    return e.Salary * 0.1
}

// Or with named field
type Employee struct {
    Person Person
    Salary float64
}

func (e Employee) Bonus() float64 {
    return e.Salary * 0.1
}

Key Difference: Go favors composition. No implicit inheritance—everything is explicit.


Error Handling

This is where Python and Go differ most.

Python: Exceptions

# Python - try/except
def read_config(path: str) -> dict:
    try:
        with open(path) as f:
            return json.load(f)
    except FileNotFoundError:
        return {}
    except json.JSONDecodeError as e:
        logger.error(f"Invalid JSON: {e}")
        raise ConfigurationError(f"Invalid config: {e}")
    except Exception as e:
        logger.error(f"Unexpected error: {e}")
        raise

# Calling code
try:
    config = read_config("config.json")
except ConfigurationError:
    config = {}

Go: Explicit Errors

// Go - explicit error returns
func ReadConfig(path string) (map[string]interface{}, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        if errors.Is(err, os.ErrNotExist) {
            return map[string]interface{}{}, nil
        }
        return nil, fmt.Errorf("failed to read file: %w", err)
    }

    var config map[string]interface{}
    if err := json.Unmarshal(data, &config); err != nil {
        return nil, fmt.Errorf("invalid JSON in %s: %w", path, err)
    }

    return config, nil
}

// Calling code
config, err := ReadConfig("config.json")
if err != nil {
    log.Printf("Using default config: %v", err)
    config = map[string]interface{}{}
}

Key Differences:

  • Go errors are values, not exceptions
  • Must check errors explicitly
  • No stack traces by default (can add with fmt.Errorf + %w)
  • Forces you to think about failure cases

Error Handling Patterns

// Pattern 1: Check and return
func DoSomething() error {
    result, err := Step1()
    if err != nil {
        return err
    }

    if err := Step2(result); err != nil {
        return err
    }

    return nil
}

// Pattern 2: Wrapped errors with context
func ProcessUser(id string) (*User, error) {
    user, err := db.GetUser(id)
    if err != nil {
        return nil, fmt.Errorf("failed to get user %s: %w", id, err)
    }

    if err := enrichUser(user); err != nil {
        return nil, fmt.Errorf("failed to enrich user %s: %w", id, err)
    }

    return user, nil
}

// Pattern 3: Sentinel errors
var ErrUserNotFound = errors.New("user not found")

func GetUser(id string) (*User, error) {
    user, err := db.Query(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrUserNotFound
        }
        return nil, err
    }
    return user, nil
}

// Check sentinel
user, err := GetUser("123")
if errors.Is(err, ErrUserNotFound) {
    // Handle not found
}

Concurrency

This is where Go shines.

Python: Asyncio

# Python - asyncio
import asyncio
import aiohttp

async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
    async with session.get(url) as response:
        return await response.text()

async def main():
    urls = [
        "https://api1.example.com",
        "https://api2.example.com",
        "https://api3.example.com",
    ]

    async with aiohttp.ClientSession() as session:
        # Fetch concurrently
        tasks = [fetch_url(session, url) for url in urls]
        results = await asyncio.gather(*tasks)

    return results

# Run
asyncio.run(main())

Go: Goroutines

// Go - goroutines
package main

import (
    "fmt"
    "net/http"
    "sync"
)

func fetchURL(url string) (string, error) {
    resp, err := http.Get(url)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    // Read body...
    return "result", nil
}

func main() {
    urls := []string{
        "https://api1.example.com",
        "https://api2.example.com",
        "https://api3.example.com",
    }

    // Fetch concurrently
    var wg sync.WaitGroup
    results := make(chan string, len(urls))

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            result, err := fetchURL(u)
            if err != nil {
                results <- fmt.Sprintf("Error: %v", err)
                return
            }
            results <- result
        }(url)
    }

    // Close channel when done
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    for result := range results {
        fmt.Println(result)
    }
}

Key Differences:

  • Goroutines are lightweight (vs threads in Python)
  • Channels for communication (vs futures/promises)
  • No event loop to manage
  • Simpler mental model

Goroutine Patterns

// Pattern 1: Worker pool
func WorkerPool(jobs []Job, numWorkers int) []Result {
    jobCh := make(chan Job)
    resultCh := make(chan Result, len(jobs))

    // Start workers
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobCh {
                resultCh <- process(job)
            }
        }()
    }

    // Send jobs
    go func() {
        for _, job := range jobs {
            jobCh <- job
        }
        close(jobCh)
    }()

    // Wait and collect
    go func() {
        wg.Wait()
        close(resultCh)
    }()

    var results []Result
    for r := range resultCh {
        results = append(results, r)
    }

    return results
}

// Pattern 2: Context for cancellation
func FetchWithTimeout(ctx context.Context, url string) (string, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return "", err
    }

    client := &http.Client{Timeout: 10 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    // ...
    return "result", nil
}

// Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := FetchWithTimeout(ctx, "https://api.example.com")

Interfaces

Python: Duck Typing

# Python - implicit interfaces via duck typing
class FileStore:
    def get(self, key: str) -> bytes:
        with open(key, 'rb') as f:
            return f.read()

    def put(self, key: str, value: bytes) -> None:
        with open(key, 'wb') as f:
            f.write(value)

class S3Store:
    def get(self, key: str) -> bytes:
        return s3_client.get_object(key)

    def put(self, key: str, value: bytes) -> None:
        s3_client.put_object(key, value)

# Both work with the same code
def process_store(store, key: str):
    data = store.get(key)
    # ...
    store.put(key, data)

Go: Explicit Interfaces

// Go - explicit interface definition
type Store interface {
    Get(key string) ([]byte, error)
    Put(key string, value []byte) error
}

// FileStore implements Store (implicitly!)
type FileStore struct {
    BasePath string
}

func (fs FileStore) Get(key string) ([]byte, error) {
    return os.ReadFile(filepath.Join(fs.BasePath, key))
}

func (fs FileStore) Put(key string, value []byte) error {
    return os.WriteFile(filepath.Join(fs.BasePath, key), value, 0644)
}

// S3Store also implements Store
type S3Store struct {
    Client *s3.Client
    Bucket string
}

func (s S3Store) Get(key string) ([]byte, error) {
    // S3 implementation...
    return nil, nil
}

func (s S3Store) Put(key string, value []byte) error {
    // S3 implementation...
    return nil
}

// Function accepts any Store
func ProcessStore(store Store, key string) error {
    data, err := store.Get(key)
    if err != nil {
        return err
    }
    // ...
    return store.Put(key, data)
}

// Compile-time check
var _ Store = FileStore{} // Fails if FileStore doesn't implement Store

Key Differences:

  • Go interfaces are satisfied implicitly (no implements keyword)
  • Compile-time checking
  • More explicit but also more flexible

Building a Real Project

Let’s build a URL shortener API in Go (equivalent to a Flask app).

Project Structure

urlshortener/
├── main.go
├── go.mod
├── go.sum
├── handlers/
│   └── url.go
├── storage/
│   ├── memory.go
│   └── redis.go
├── models/
│   └── url.go
└── go.mod

The Code

// models/url.go
package models

type URL struct {
    ID       string `json:"id"`
    LongURL  string `json:"long_url"`
    ShortCode string `json:"short_code"`
    Clicks   int    `json:"clicks"`
}

// storage/store.go
package storage

type Store interface {
    Save(url *models.URL) error
    Get(shortCode string) (*models.URL, error)
    IncrementClicks(shortCode string) error
}

// storage/memory.go
package storage

type MemoryStore struct {
    mu   sync.RWMutex
    urls map[string]*models.URL
}

func NewMemoryStore() *MemoryStore {
    return &MemoryStore{
        urls: make(map[string]*models.URL),
    }
}

func (s *MemoryStore) Save(url *models.URL) error {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.urls[url.ShortCode] = url
    return nil
}

func (s *MemoryStore) Get(shortCode string) (*models.URL, error) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    url, exists := s.urls[shortCode]
    if !exists {
        return nil, fmt.Errorf("URL not found")
    }
    return url, nil
}

// handlers/url.go
package handlers

type URLHandler struct {
    store  storage.Store
    baseURL string
}

func NewURLHandler(store storage.Store, baseURL string) *URLHandler {
    return &URLHandler{
        store:   store,
        baseURL: baseURL,
    }
}

func (h *URLHandler) CreateShortURL(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    var req struct {
        LongURL string `json:"long_url"`
    }

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    shortCode := generateShortCode()
    url := &models.URL{
        ID:        uuid.New().String(),
        LongURL:   req.LongURL,
        ShortCode: shortCode,
    }

    if err := h.store.Save(url); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    resp := struct {
        ShortURL string `json:"short_url"`
    }{
        ShortURL: h.baseURL + "/" + shortCode,
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(resp)
}

// main.go
package main

import (
    "log"
    "net/http"
    "os"

    "urlshortener/handlers"
    "urlshortener/storage"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    store := storage.NewMemoryStore()
    handler := handlers.NewURLHandler(store, "http://localhost:"+port)

    http.HandleFunc("/api/shorten", handler.CreateShortURL)
    http.HandleFunc("/", handler.Redirect)

    log.Printf("Server starting on port %s", port)
    if err := http.ListenAndServe(":"+port, nil); err != nil {
        log.Fatal(err)
    }
}

Running the Project

# Initialize
mkdir urlshortener && cd urlshortener
go mod init urlshortener

# Add dependencies
go get github.com/google/uuid

# Run
go run main.go

# Build binary
go build -o urlshortener

# Run binary
./urlshortener

Common Python Habits to Unlearn

1. Dynamic Attribute Access

// ❌ Don't do this
data["key"]  // Python dict access

// ✅ Do this
data["key"] // Go map access (same syntax, but types matter)

// ❌ Dynamic attributes
user.name = "Alice" // Works in Python

// ✅ Go struct
user.Name = "Alice" // Must use exact field name

2. Implicit Returns

// ❌ Python: returns None implicitly
def greet(name):
    print(f"Hello, {name}")

// ✅ Go: must return explicitly
func greet(name string) string {
    return "Hello, " + name
}

3. Exception Handling

// ❌ Python thinking
try:
    result := DoSomething()
except Exception as e:
    log.error(e)

// ✅ Go thinking
result, err := DoSomething()
if err != nil {
    log.Printf("Error: %v", err)
    return
}
// Must check err before using result

4. List Comprehensions

// ❌ Pythonic thinking
// results = [process(x) for x in items]

// ✅ Go thinking
var results []Result
for _, item := range items {
    results = append(results, process(item))
}

5. Monkey Patching

// ❌ Python: can patch at runtime
# SomeClass.method = new_method

// ✅ Go: can't patch; use interfaces
// Define interface, create new implementation

Testing in Go

Python vs Go

# Python - pytest
def test_add():
    assert add(2, 3) == 5

def test_add_negative():
    assert add(-1, -2) == -3
// Go - built-in testing
package mypkg

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Add(2, 3) = %d; want 5", result)
    }
}

func TestAddNegative(t *testing.T) {
    result := Add(-1, -2)
    if result != -3 {
        t.Errorf("Add(-1, -2) = %d; want -3", result)
    }
}

// Run tests
go test

// Verbose
go test -v

// Coverage
go test -cover

Table-Driven Tests

func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     float64
        expected float64
        wantErr  bool
    }{
        {"normal", 10, 2, 5, false},
        {"decimal", 7, 2, 3.5, false},
        {"negative", -10, 2, -5, false},
        {"divide by zero", 10, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := Divide(tt.a, tt.b)
            if tt.wantErr {
                if err == nil {
                    t.Errorf("expected error, got none")
                }
                return
            }
            if err != nil {
                t.Errorf("unexpected error: %v", err)
                return
            }
            if result != tt.expected {
                t.Errorf("got %v, want %v", result, tt.expected)
            }
        })
    }
}

Resources and Next Steps

Learning Resources

Books:

  • “The Go Programming Language” (Donovan & Kernighan) - The bible
  • “100 Go Mistakes” - Avoid common pitfalls

Courses:

  • “Ultimate Go” (Ardan Labs) - Worth every penny
  • “Go: The Complete Developer’s Guide” (Udemy)

Practice:

  • Exercism.io Go track
  • Advent of Code in Go
  • Build real projects

Project Ideas

  1. CLI Tool: Rewrite a Python script in Go
  2. API Server: Build a REST API with stdlib or Gin
  3. Worker: Background job processor with goroutines
  4. Tooling: A small dev tool (like a linter or formatter)

When to Use Go vs Python

Use Go when:

  • Building APIs/services
  • Need concurrent processing
  • Want single binary deployment
  • Performance matters
  • Working on infrastructure tools

Use Python when:

  • Data science/ML
  • Scripting/automation
  • Prototyping
  • Rich ecosystem needed
  • Team prefers it

Conclusion

Go isn’t Python—and that’s okay. Python gives you flexibility and rapid development. Go gives you simplicity, performance, and deployment ease.

The transition takes time. You’ll miss list comprehensions. You’ll curse explicit error handling. But eventually, Go’s simplicity becomes a superpower.

Start small. Rewrite a Python script in Go. Build a toy API. Embrace the explicitness. Before long, you’ll be writing idiomatic Go.

The best developers are multilingual. Adding Go to your toolkit makes you more valuable, not less.

Happy coding.


Questions about Go? I’m happy to help—reach out on Twitter or email.

Related: Rust for JavaScript Developers

Last updated: March 17, 2026. Go 1.22+ features included.

---

评论