Agents convert-python-golang

Convert Python code to idiomatic Go. Use when migrating Python projects to Go, translating Python patterns to idiomatic Go, or refactoring Python codebases for performance and concurrency. Extends meta-convert-dev with Python-to-Go specific patterns.

install
source · Clone the upstream repo
git clone https://github.com/aRustyDev/agents
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/aRustyDev/agents "$T" && mkdir -p ~/.claude/skills && cp -r "$T/content/skills/convert-python-golang" ~/.claude/skills/arustydev-agents-convert-python-golang && rm -rf "$T"
manifest: content/skills/convert-python-golang/SKILL.md
source content

Convert Python to Go

Convert Python code to idiomatic Go. This skill extends

meta-convert-dev
with Python-to-Go specific type mappings, idiom translations, and tooling for transforming dynamic, interpreted Python code into static, compiled Go.

This Skill Extends

  • meta-convert-dev
    - Foundational conversion patterns (APTV workflow, testing strategies)

For general concepts like the Analyze → Plan → Transform → Validate workflow, testing strategies, and common pitfalls, see the meta-skill first.

This Skill Adds

  • Type mappings: Python types → Go types (dynamic → static)
  • Idiom translations: Python patterns → idiomatic Go
  • Error handling: Exceptions → multiple return values with error
  • Async patterns: asyncio → goroutines and channels
  • Memory model: Python GC → Go GC (both garbage collected, different idioms)
  • Type system: Duck typing → interfaces and struct embedding
  • Build system: pip/uv → go modules

This Skill Does NOT Cover

  • General conversion methodology - see
    meta-convert-dev
  • Python language fundamentals - see
    lang-python-dev
  • Go language fundamentals - see
    lang-go-dev
  • Reverse conversion (Go → Python) - see
    convert-golang-python

Quick Reference

PythonGoNotes
int
int
,
int64
,
big.Int
Python has arbitrary precision
float
float64
IEEE 754 double precision
bool
bool
Direct mapping
str
string
Immutable UTF-8 strings
bytes
[]byte
Byte slices
list[T]
[]T
Slices (dynamic arrays)
tuple
struct{}
or array
Use struct for heterogeneous
dict[K, V]
map[K]V
Hash maps
set[T]
map[T]bool
or
map[T]struct{}
No built-in set type
None
nil
(for pointers) or zero value
Context-dependent
Optional[T]
*T
(pointer)
Pointer = nullable
Union[T, U]
interface{}
or custom type
Use type assertions
def func():
func name() {}
Functions
async def
func
+ goroutines
No async/await syntax
with
defer
Resource cleanup
@decorator
Function wrappersNo decorator syntax
class
type
+ struct
Composition over inheritance
Exception
error
return value
Multiple return values

When Converting Code

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table
  3. Handle arbitrary-precision integers - decide if
    int64
    is enough or need
    big.Int
  4. Preserve semantics over syntax similarity
  5. Adopt Go idioms - don't write "Python code in Go syntax"
  6. Handle edge cases - None, exceptions, dynamic typing assumptions
  7. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

PythonGoNotes
int
int
Platform-dependent (32 or 64-bit)
int
int64
Explicit 64-bit for large numbers
int
math/big.Int
Python default - arbitrary precision
float
float64
IEEE 754 double precision
bool
bool
Direct mapping
str
string
Immutable UTF-8 strings
bytes
[]byte
Byte slice
bytearray
[]byte
Mutable byte slice
None
nil
For pointers, slices, maps, channels
None
Zero valueFor value types (0, false, "")

Critical Note on Integers: Python's

int
type has arbitrary precision and never overflows. Go integers are fixed-size and can overflow. Always validate range or use
math/big.Int
for Python-like behavior.

Collection Types

PythonGoNotes
list[T]
[]T
Slice (dynamic array)
tuple[T, U]
struct{X T; Y U}
Heterogeneous tuple → struct
tuple[T, ...]
[]T
Homogeneous tuple → slice
dict[K, V]
map[K]V
Hash map
set[T]
map[T]bool
Set as map keys
set[T]
map[T]struct{}
Memory-efficient set
frozenset[T]
map[T]struct{}
Go maps are mutable
collections.deque
container/list.List
Doubly-linked list
collections.OrderedDict
Map iteration (Go 1.12+)Maps preserve insertion order in Go 1.12+
collections.defaultdict
Map with checkUse map access pattern
collections.Counter
map[T]int
Count occurrences

Composite Types

PythonGoNotes
class
(data)
struct
Data containers
class
(behavior)
interface
Behavior contracts
@dataclass
struct
with literal
Simple data structures
typing.Protocol
interface
Duck typing → interfaces
typing.TypedDict
struct
Named fields
typing.NamedTuple
struct
Prefer struct
enum.Enum
const
with iota
Enumerated constants
typing.Literal["a", "b"]
const
or custom type
Literal types
typing.Union[T, U]
interface{}
+ type assertion
Or custom type
typing.Optional[T]
*T
(pointer)
Pointer = nullable
typing.Callable[[Args], Ret]
func(Args) Ret
Function types
typing.Generic[T]
InterfacesGo 1.18+ generics limited

Type Annotations → Interfaces

PythonGoNotes
def f(x: Iterable[T])
func f(x []T)
Slice for most cases
def f(x: Sequence[T])
func f(x []T)
Slice for sequences
def f(x: Mapping[K, V])
func f(x map[K]V)
Map for mappings
x: Any
interface{}
(or
any
)
Use sparingly

Idiom Translation

Pattern 1: None Handling (Optional Values)

Python:

# Optional chaining
user = get_user(user_id)
if user is not None:
    name = user.name
else:
    name = "Anonymous"

# Or with walrus operator
if user := get_user(user_id):
    name = user.name

Go:

// Pointer nil check
user := getUser(userID)
var name string
if user != nil {
    name = user.Name
} else {
    name = "Anonymous"
}

// Or with early return
user := getUser(userID)
if user == nil {
    name = "Anonymous"
} else {
    name = user.Name
}

Why this translation:

  • Python uses
    None
    with truthiness; Go uses
    nil
    with explicit pointer checks
  • Go's zero values provide defaults without needing
    None
    for primitives
  • Go pointers are explicit about nullability

Pattern 2: List Comprehensions → Slice Loops

Python:

# List comprehension
squared_evens = [x * x for x in numbers if x % 2 == 0]

# Generator expression
total = sum(x * x for x in numbers if x % 2 == 0)

Go:

// Slice with filtering and mapping
var squaredEvens []int
for _, x := range numbers {
    if x % 2 == 0 {
        squaredEvens = append(squaredEvens, x*x)
    }
}

// Manual aggregation
total := 0
for _, x := range numbers {
    if x % 2 == 0 {
        total += x * x
    }
}

Why this translation:

  • Go doesn't have list comprehensions; use explicit loops
  • range
    loops are idiomatic for iteration
  • append
    grows slices dynamically (similar to Python lists)

Pattern 3: Dictionary Operations

Python:

# Get with default
value = config.get("timeout", 30)

# Setdefault pattern
cache.setdefault(key, expensive_compute())

# Dictionary comprehension
squared = {k: v * v for k, v in items.items()}

Go:

// Get with default
value, ok := config["timeout"]
if !ok {
    value = 30
}

// Check and set pattern
if _, ok := cache[key]; !ok {
    cache[key] = expensiveCompute()
}

// Manual map building
squared := make(map[string]int)
for k, v := range items {
    squared[k] = v * v
}

Why this translation:

  • Go's two-value map access (
    value, ok := map[key]
    ) checks existence
  • No built-in
    get
    method; use existence check pattern
  • Explicit loops replace comprehensions

Pattern 4: String Formatting

Python:

# f-strings
message = f"User {user.name} has {count} items"

# format method
message = "User {} has {} items".format(user.name, count)

Go:

// fmt.Sprintf (returns string)
message := fmt.Sprintf("User %s has %d items", user.Name, count)

// fmt.Printf (prints directly)
fmt.Printf("User %s has %d items\n", user.Name, count)

Why this translation:

  • Go uses
    fmt
    package with C-style format specifiers
  • %s
    for strings,
    %d
    for integers,
    %v
    for default format
  • Type-safe at runtime (not compile-time like some languages)

Pattern 5: Duck Typing → Interfaces

Python:

# Duck typing - if it has .read(), it's file-like
def process_data(file_like):
    data = file_like.read()
    return parse(data)

# Works with files, StringIO, BytesIO, etc.

Go:

// Interface definition
type Reader interface {
    Read(p []byte) (n int, err error)
}

// Function accepts interface
func processData(r Reader) (Data, error) {
    data, err := io.ReadAll(r)
    if err != nil {
        return Data{}, err
    }
    return parse(data)
}

// Works with *os.File, bytes.Buffer, strings.Reader, etc.

Why this translation:

  • Python relies on runtime duck typing; Go uses compile-time interfaces
  • Go interfaces are implicit (no "implements" keyword)
  • More type-safe but requires upfront interface definition

Pattern 6: Context Managers → Defer

Python:

# Context manager for resource cleanup
with open("file.txt") as f:
    data = f.read()
# File automatically closed

Go:

// Defer for cleanup
f, err := os.Open("file.txt")
if err != nil {
    return err
}
defer f.Close() // Guaranteed to run when function returns

data, err := io.ReadAll(f)
if err != nil {
    return err
}

Why this translation:

  • Python's
    with
    guarantees cleanup via
    __exit__
  • Go's
    defer
    schedules function calls for later (LIFO order)
  • Both ensure cleanup even with errors/returns

Pattern 7: Decorators → Function Wrappers

Python:

from functools import wraps

def log_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__}")
        result = func(*args, **kwargs)
        print(f"Finished {func.__name__}")
        return result
    return wrapper

@log_calls
def process_data(data):
    return len(data)

Go:

// Function wrapper pattern
func logCalls(name string, fn func([]byte) int) func([]byte) int {
    return func(data []byte) int {
        fmt.Printf("Calling %s\n", name)
        result := fn(data)
        fmt.Printf("Finished %s\n", name)
        return result
    }
}

// Manual wrapping
processData := logCalls("processData", func(data []byte) int {
    return len(data)
})

Why this translation:

  • Go doesn't have decorator syntax; use function wrappers
  • Requires explicit wrapping instead of
    @
    syntax
  • Type signatures must match exactly (less flexible than Python)

Pattern 8: Class Methods and Properties

Python:

class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def area(self):
        return 3.14159 * self._radius ** 2

    @property
    def radius(self):
        return self._radius

    @radius.setter
    def radius(self, value):
        if value < 0:
            raise ValueError("Radius cannot be negative")
        self._radius = value

Go:

type Circle struct {
    radius float64
}

// Constructor
func NewCircle(radius float64) *Circle {
    return &Circle{radius: radius}
}

// Getter (no "Get" prefix in Go)
func (c *Circle) Area() float64 {
    return 3.14159 * c.radius * c.radius
}

func (c *Circle) Radius() float64 {
    return c.radius
}

// Setter
func (c *Circle) SetRadius(value float64) error {
    if value < 0 {
        return errors.New("radius cannot be negative")
    }
    c.radius = value
    return nil
}

Why this translation:

  • Go doesn't have properties; use methods
  • No automatic getter/setter generation
  • Explicit error returns instead of exceptions

Error Handling

Python Exceptions → Go Multiple Returns

Python:

def divide(a, b):
    if b == 0:
        raise ValueError("division by zero")
    return a / b

# Caller
try:
    result = divide(10, 2)
    print(f"Result: {result}")
except ValueError as e:
    print(f"Error: {e}")

Go:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Caller
result, err := divide(10, 2)
if err != nil {
    fmt.Printf("Error: %v\n", err)
    return
}
fmt.Printf("Result: %f\n", result)

Translation rules:

  • Python raises exceptions; Go returns
    (result, error)
  • Check
    err != nil
    immediately after function call
  • Return early on error (guard clauses)
  • nil
    error means success

Custom Exceptions → Custom Error Types

Python:

class ValidationError(Exception):
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

raise ValidationError("email", "invalid format")

Go:

type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("%s: %s", e.Field, e.Message)
}

// Return custom error
return ValidationError{Field: "email", Message: "invalid format"}

Translation rules:

  • Implement
    Error() string
    method to satisfy
    error
    interface
  • Add fields for error context
  • Use
    errors.Is()
    and
    errors.As()
    for error type checking

Exception Hierarchies → Error Wrapping

Python:

try:
    process_data()
except ValueError as e:
    raise ConfigError(f"Invalid config: {e}") from e

Go:

err := processData()
if err != nil {
    return fmt.Errorf("invalid config: %w", err)
}

// Check wrapped error
if errors.Is(err, os.ErrNotExist) {
    // Handle specific error
}

Translation rules:

  • Use
    %w
    in
    fmt.Errorf
    to wrap errors
  • errors.Is()
    checks error identity (unwraps chain)
  • errors.As()
    extracts specific error types

Concurrency Patterns

Asyncio → Goroutines and Channels

Python:

import asyncio

async def fetch_user(user_id):
    await asyncio.sleep(0.1)  # Simulate I/O
    return {"id": user_id, "name": f"User {user_id}"}

async def main():
    # Sequential
    user1 = await fetch_user(1)
    user2 = await fetch_user(2)

    # Concurrent
    users = await asyncio.gather(
        fetch_user(1),
        fetch_user(2),
        fetch_user(3)
    )
    print(users)

asyncio.run(main())

Go:

import "time"

func fetchUser(userID int) User {
    time.Sleep(100 * time.Millisecond) // Simulate I/O
    return User{ID: userID, Name: fmt.Sprintf("User %d", userID)}
}

func main() {
    // Sequential
    user1 := fetchUser(1)
    user2 := fetchUser(2)

    // Concurrent with goroutines and channels
    ch := make(chan User, 3)
    for _, id := range []int{1, 2, 3} {
        id := id // Capture loop variable
        go func() {
            ch <- fetchUser(id)
        }()
    }

    // Collect results
    var users []User
    for i := 0; i < 3; i++ {
        users = append(users, <-ch)
    }
    fmt.Println(users)
}

Why this translation:

  • Python uses
    async/await
    syntax; Go uses goroutines (lightweight threads)
  • Go channels replace asyncio coordination
  • No event loop - goroutines are managed by runtime

Threading → Goroutines with Sync

Python:

from concurrent.futures import ThreadPoolExecutor

def fetch_url(url):
    # I/O-bound operation
    return f"Content from {url}"

urls = ["http://example.com", "http://example.org"]

with ThreadPoolExecutor(max_workers=5) as executor:
    results = list(executor.map(fetch_url, urls))

Go:

import "sync"

func fetchURL(url string) string {
    // I/O-bound operation
    return fmt.Sprintf("Content from %s", url)
}

func main() {
    urls := []string{"http://example.com", "http://example.org"}

    var wg sync.WaitGroup
    results := make([]string, len(urls))

    for i, url := range urls {
        wg.Add(1)
        go func(i int, url string) {
            defer wg.Done()
            results[i] = fetchURL(url)
        }(i, url)
    }

    wg.Wait()
    fmt.Println(results)
}

Why this translation:

  • Python ThreadPoolExecutor → Go goroutines + WaitGroup
  • Go's goroutines are cheaper than OS threads
  • sync.WaitGroup
    waits for goroutines to complete

Multiprocessing → Goroutines (Usually)

Python:

from multiprocessing import Pool

def cpu_bound_task(n):
    return sum(i * i for i in range(n))

with Pool(4) as pool:
    results = pool.map(cpu_bound_task, [1000000] * 4)

Go:

func cpuBoundTask(n int) int {
    sum := 0
    for i := 0; i < n; i++ {
        sum += i * i
    }
    return sum
}

func main() {
    runtime.GOMAXPROCS(4) // Optional: limit CPU cores

    var wg sync.WaitGroup
    results := make([]int, 4)

    for i := 0; i < 4; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            results[i] = cpuBoundTask(1000000)
        }(i)
    }

    wg.Wait()
    fmt.Println(results)
}

Why this translation:

  • Python uses multiprocessing to bypass GIL for CPU-bound tasks
  • Go's goroutines can run in parallel on multiple cores (no GIL)
  • Same pattern (goroutines) works for both I/O and CPU-bound tasks

Paradigm Translation

Mental Model Shift: Dynamic → Static Typing

Python ConceptGo ApproachKey Insight
Duck typingInterfacesType contracts defined upfront
Dynamic attributesStruct fieldsFixed structure
**kwargs
flexibility
Options patternFunctional options for flexibility
Multiple inheritanceComposition + embeddingFavor composition
Monkey patchingAvoidBuild-time dependency injection

Object-Oriented → Struct-Based

Python:

class Animal:
    def speak(self):
        raise NotImplementedError

class Dog(Animal):
    def speak(self):
        return "Woof!"

class Cat(Animal):
    def speak(self):
        return "Meow!"

Go:

// Interface defines behavior
type Animal interface {
    Speak() string
}

// Structs implement interface implicitly
type Dog struct{}

func (d Dog) Speak() string {
    return "Woof!"
}

type Cat struct{}

func (c Cat) Speak() string {
    return "Meow!"
}

// Use interface for polymorphism
func makeSound(a Animal) {
    fmt.Println(a.Speak())
}

Key differences:

  • Go has no classes or inheritance
  • Interfaces are implicit (no "implements" keyword)
  • Composition via struct embedding

Module and Import System

Python Imports → Go Packages

Python:

# myproject/utils/strings.py
def capitalize(s):
    return s.upper()

# myproject/main.py
from utils.strings import capitalize
# Or
from utils import strings

Go:

// myproject/utils/strings.go
package utils

func Capitalize(s string) string {
    return strings.ToUpper(s)
}

// myproject/main.go
package main

import "myproject/utils"

func main() {
    result := utils.Capitalize("hello")
}

Translation rules:

  • Python modules are files; Go packages are directories
  • Uppercase names are exported (public) in Go
  • Import path is directory path, not file path

Python init.py → Go Package Organization

Python:

# mypackage/__init__.py
from .module_a import ClassA
from .module_b import ClassB

__all__ = ["ClassA", "ClassB"]

Go:

// All .go files in same directory are same package
// No __init__.go equivalent

// mypackage/module_a.go
package mypackage

type ClassA struct {}

// mypackage/module_b.go
package mypackage

type ClassB struct {}

// Exported names (uppercase) are automatically public

Translation rules:

  • Go has no
    __init__
    file
  • All
    .go
    files in a directory must have same
    package
    declaration
  • Visibility controlled by capitalization

Build and Dependencies

pip/uv → go modules

Python:

# requirements.txt
requests==2.31.0
pytest>=7.4.0

# Install
pip install -r requirements.txt

Go:

# go.mod (generated)
module github.com/user/myproject

go 1.21

require (
    github.com/gorilla/mux v1.8.0
)

# Add dependency
go get github.com/gorilla/mux@v1.8.0

# Update all
go get -u ./...

# Cleanup
go mod tidy

Translation rules:

  • go.mod
    is like
    pyproject.toml
    (declarative)
  • go.sum
    is like lock file (checksums)
  • No virtual environments needed (per-project modules)

Project Structure

Python:

myproject/
├── pyproject.toml
├── src/
│   └── myproject/
│       ├── __init__.py
│       └── core.py
└── tests/
    └── test_core.py

Go:

myproject/
├── go.mod
├── go.sum
├── main.go
├── internal/        # Private packages
│   └── core/
│       └── core.go
└── core_test.go     # Tests alongside code

Translation rules:

  • Go tests live next to source (
    *_test.go
    )
  • internal/
    packages can't be imported by external projects
  • Flat structure preferred over deep nesting

Testing

pytest → testing package

Python:

# test_math.py
import pytest

def test_add():
    assert add(1, 2) == 3

@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (-1, 1, 0),
])
def test_add_parametrized(a, b, expected):
    assert add(a, b) == expected

@pytest.fixture
def sample_data():
    return [1, 2, 3]

def test_with_fixture(sample_data):
    assert len(sample_data) == 3

Go:

// math_test.go
package math

import "testing"

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

// Table-driven test (idiomatic Go)
func TestAddTable(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive", 1, 2, 3},
        {"negative", -1, 1, 0},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d",
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

// Test helper (like fixture)
func setupData(t *testing.T) []int {
    t.Helper()
    data := []int{1, 2, 3}
    t.Cleanup(func() {
        // Cleanup if needed
    })
    return data
}

func TestWithHelper(t *testing.T) {
    data := setupData(t)
    if len(data) != 3 {
        t.Errorf("len(data) = %d; want 3", len(data))
    }
}

Translation rules:

  • pytest
    go test
    (built-in)
  • Parametrize → table-driven tests (idiomatic)
  • Fixtures → helper functions with
    t.Helper()

Common Pitfalls

  1. Forgetting to Check Errors

Python exceptions are automatic; Go errors must be checked explicitly.

// Bad
result, _ := doSomething() // Ignoring error

// Good
result, err := doSomething()
if err != nil {
    return err
}
  1. Treating nil Like None

Python

None
can be used for any type; Go
nil
only works with pointers, slices, maps, channels, functions, and interfaces.

// Bad
var name string = nil  // Compile error

// Good
var name *string = nil  // OK - pointer
var name string         // OK - zero value ""
  1. Modifying Loop Variables in Goroutines
// Bad
for _, item := range items {
    go func() {
        process(item) // All goroutines see last item!
    }()
}

// Good
for _, item := range items {
    item := item // Capture loop variable
    go func() {
        process(item)
    }()
}
  1. Expecting Reference Semantics for Structs

Python objects are references; Go structs are values (copy on assignment).

// Bad
func updateUser(u User) {
    u.Name = "Updated" // Modifies copy, not original
}

// Good
func updateUser(u *User) {
    u.Name = "Updated" // Modifies original via pointer
}
  1. Integer Overflow

Python integers never overflow; Go integers wrap or panic.

var x int64 = 9223372036854775807 // Max int64
x = x + 1 // Overflows to negative in production

// Use math/big for arbitrary precision
import "math/big"
x := big.NewInt(9223372036854775807)
x.Add(x, big.NewInt(1)) // No overflow
  1. Map Iteration Order

Python dicts maintain insertion order (3.7+); Go maps have random iteration order.

// Order is not guaranteed
for key, value := range myMap {
    // Don't rely on order
}

// Use slice of keys for consistent order
keys := make([]string, 0, len(myMap))
for key := range myMap {
    keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
    // Ordered iteration
}
  1. Shadowing Variables with :=
// Bad
user, err := getUser(1)
if err != nil {
    user, err := getUser(2) // Shadows outer user!
    // ...
}
// user is still from getUser(1)

// Good
user, err := getUser(1)
if err != nil {
    user, err = getUser(2) // Reuses outer user
    // ...
}

Serialization

JSON: Python json → Go encoding/json

Python:

import json
from dataclasses import dataclass

@dataclass
class User:
    name: str
    email: str
    age: int

user = User(name="Alice", email="alice@example.com", age=30)

# Serialize
json_str = json.dumps(user.__dict__)

# Deserialize
data = json.loads(json_str)
user = User(**data)

Go:

import "encoding/json"

type User struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

user := User{Name: "Alice", Email: "alice@example.com", Age: 30}

// Serialize
data, err := json.Marshal(user)
if err != nil {
    return err
}

// Deserialize
var user User
err = json.Unmarshal(data, &user)
if err != nil {
    return err
}

Translation rules:

  • Use struct tags for field names:
    json:"field_name"
  • json:",omitempty"
    for optional fields
  • Pointers for nullable fields

See also:

patterns-serialization-dev
for cross-language comparison


Zero Values and Defaults

Python None → Go Zero Values

Python:

# None represents absence
value = None

# Check for None
if value is None:
    value = "default"

Go:

// Zero values for each type
var i int       // 0
var f float64   // 0.0
var s string    // ""
var p *int      // nil
var slice []int // nil
var m map[K]V   // nil

// Check for nil (pointers, slices, maps)
if slice == nil {
    slice = []int{1, 2, 3}
}

// Check for zero value (strings, numbers)
if s == "" {
    s = "default"
}

Translation rules:

  • Pointers, slices, maps, channels, functions →
    nil
  • Numbers →
    0
    or
    0.0
  • Strings →
    ""
  • Bools →
    false
  • Structs → all fields zero-valued

Metaprogramming

Python Decorators → Go Code Generation

Python has runtime metaprogramming; Go uses compile-time code generation.

Python:

# Runtime decorator
def memoize(func):
    cache = {}
    def wrapper(*args):
        if args not in cache:
            cache[args] = func(*args)
        return cache[args]
    return wrapper

@memoize
def fib(n):
    if n < 2:
        return n
    return fib(n-1) + fib(n-2)

Go:

// Manual memoization (no decorator syntax)
var fibCache = make(map[int]int)

func fib(n int) int {
    if result, ok := fibCache[n]; ok {
        return result
    }

    if n < 2 {
        return n
    }

    result := fib(n-1) + fib(n-2)
    fibCache[n] = result
    return result
}

// Or use code generation for repetitive patterns
//go:generate mockgen -source=interface.go -destination=mock.go

Translation rules:

  • No decorator syntax in Go
  • Use function wrappers or code generation
  • go generate
    for compile-time metaprogramming

See also:

patterns-metaprogramming-dev
for cross-language comparison


Examples

Example 1: Simple - HTTP Client

Before (Python):

import requests

def fetch_user(user_id: int) -> dict:
    response = requests.get(f"https://api.example.com/users/{user_id}")
    response.raise_for_status()
    return response.json()

try:
    user = fetch_user(1)
    print(f"User: {user['name']}")
except requests.HTTPError as e:
    print(f"HTTP error: {e}")
except Exception as e:
    print(f"Error: {e}")

After (Go):

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

type User struct {
    Name string `json:"name"`
}

func fetchUser(userID int) (User, error) {
    url := fmt.Sprintf("https://api.example.com/users/%d", userID)
    resp, err := http.Get(url)
    if err != nil {
        return User{}, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return User{}, fmt.Errorf("HTTP error: %d", resp.StatusCode)
    }

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return User{}, err
    }

    var user User
    if err := json.Unmarshal(body, &user); err != nil {
        return User{}, err
    }

    return user, nil
}

func main() {
    user, err := fetchUser(1)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("User: %s\n", user.Name)
}

Example 2: Medium - Concurrent Workers

Before (Python):

import asyncio
from typing import List

async def process_item(item: int) -> int:
    await asyncio.sleep(0.1)  # Simulate work
    return item * 2

async def process_batch(items: List[int]) -> List[int]:
    tasks = [process_item(item) for item in items]
    return await asyncio.gather(*tasks)

async def main():
    items = list(range(10))
    results = await process_batch(items)
    print(f"Processed {len(results)} items")

asyncio.run(main())

After (Go):

import (
    "fmt"
    "sync"
    "time"
)

func processItem(item int) int {
    time.Sleep(100 * time.Millisecond) // Simulate work
    return item * 2
}

func processBatch(items []int) []int {
    results := make([]int, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        go func(i, item int) {
            defer wg.Done()
            results[i] = processItem(item)
        }(i, item)
    }

    wg.Wait()
    return results
}

func main() {
    items := make([]int, 10)
    for i := range items {
        items[i] = i
    }

    results := processBatch(items)
    fmt.Printf("Processed %d items\n", len(results))
}

Example 3: Complex - REST API Server

Before (Python):

from flask import Flask, jsonify, request
from dataclasses import dataclass
from typing import Dict, List, Optional

@dataclass
class User:
    id: int
    name: str
    email: str

app = Flask(__name__)
users: Dict[int, User] = {}
next_id = 1

@app.route('/users', methods=['GET'])
def list_users():
    return jsonify([user.__dict__ for user in users.values()])

@app.route('/users/<int:user_id>', methods=['GET'])
def get_user(user_id: int):
    user = users.get(user_id)
    if not user:
        return jsonify({"error": "User not found"}), 404
    return jsonify(user.__dict__)

@app.route('/users', methods=['POST'])
def create_user():
    global next_id
    data = request.get_json()

    if not data.get('name') or not data.get('email'):
        return jsonify({"error": "Missing required fields"}), 400

    user = User(id=next_id, name=data['name'], email=data['email'])
    users[next_id] = user
    next_id += 1

    return jsonify(user.__dict__), 201

if __name__ == '__main__':
    app.run(debug=True)

After (Go):

import (
    "encoding/json"
    "fmt"
    "net/http"
    "strconv"
    "sync"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type Server struct {
    mu     sync.RWMutex
    users  map[int]User
    nextID int
}

func NewServer() *Server {
    return &Server{
        users:  make(map[int]User),
        nextID: 1,
    }
}

func (s *Server) handleListUsers(w http.ResponseWriter, r *http.Request) {
    s.mu.RLock()
    defer s.mu.RUnlock()

    users := make([]User, 0, len(s.users))
    for _, user := range s.users {
        users = append(users, user)
    }

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

func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) {
    idStr := r.URL.Query().Get("id")
    id, err := strconv.Atoi(idStr)
    if err != nil {
        http.Error(w, "Invalid user ID", http.StatusBadRequest)
        return
    }

    s.mu.RLock()
    user, ok := s.users[id]
    s.mu.RUnlock()

    if !ok {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }

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

func (s *Server) handleCreateUser(w http.ResponseWriter, r *http.Request) {
    var data struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

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

    if data.Name == "" || data.Email == "" {
        http.Error(w, "Missing required fields", http.StatusBadRequest)
        return
    }

    s.mu.Lock()
    user := User{
        ID:    s.nextID,
        Name:  data.Name,
        Email: data.Email,
    }
    s.users[s.nextID] = user
    s.nextID++
    s.mu.Unlock()

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(user)
}

func main() {
    server := NewServer()

    http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            server.handleListUsers(w, r)
        case http.MethodPost:
            server.handleCreateUser(w, r)
        default:
            http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        }
    })

    fmt.Println("Server starting on :8080")
    http.ListenAndServe(":8080", nil)
}

See Also

For more examples and patterns, see:

  • meta-convert-dev
    - Foundational patterns with cross-language examples
  • convert-golang-python
    - Reverse conversion (Go → Python)
  • lang-python-dev
    - Python development patterns
  • lang-go-dev
    - Go development patterns

Cross-cutting pattern skills:

  • patterns-concurrency-dev
    - Async patterns, threading, goroutines across languages
  • patterns-serialization-dev
    - JSON, validation, struct tags across languages
  • patterns-metaprogramming-dev
    - Decorators, code generation across languages