| name | convert-python-golang |
| description | 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. |
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
| Python | Go | Notes |
|---|---|---|
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 wrappers | No decorator syntax |
class |
type + struct |
Composition over inheritance |
| Exception | error return value |
Multiple return values |
When Converting Code
- Analyze source thoroughly before writing target
- Map types first - create type equivalence table
- Handle arbitrary-precision integers - decide if
int64is enough or needbig.Int - Preserve semantics over syntax similarity
- Adopt Go idioms - don't write "Python code in Go syntax"
- Handle edge cases - None, exceptions, dynamic typing assumptions
- Test equivalence - same inputs → same outputs
Type System Mapping
Primitive Types
| Python | Go | Notes |
|---|---|---|
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 value | For 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
| Python | Go | Notes |
|---|---|---|
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 check | Use map access pattern |
collections.Counter |
map[T]int |
Count occurrences |
Composite Types
| Python | Go | Notes |
|---|---|---|
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] |
Interfaces | Go 1.18+ generics limited |
Type Annotations → Interfaces
| Python | Go | Notes |
|---|---|---|
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
Nonewith truthiness; Go usesnilwith explicit pointer checks - Go's zero values provide defaults without needing
Nonefor 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
rangeloops are idiomatic for iterationappendgrows 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
getmethod; 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
fmtpackage with C-style format specifiers %sfor strings,%dfor integers,%vfor 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
withguarantees cleanup via__exit__ - Go's
deferschedules 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 != nilimmediately after function call - Return early on error (guard clauses)
nilerror 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() stringmethod to satisfyerrorinterface - Add fields for error context
- Use
errors.Is()anderrors.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
%winfmt.Errorfto 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/awaitsyntax; 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.WaitGroupwaits 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 Concept | Go Approach | Key Insight |
|---|---|---|
| Duck typing | Interfaces | Type contracts defined upfront |
| Dynamic attributes | Struct fields | Fixed structure |
**kwargs flexibility |
Options pattern | Functional options for flexibility |
| Multiple inheritance | Composition + embedding | Favor composition |
| Monkey patching | Avoid | Build-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
.gofiles in a directory must have samepackagedeclaration - 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.modis likepyproject.toml(declarative)go.sumis 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
- 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
}
- 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 ""
- 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)
}()
}
- 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
}
- 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
- 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
}
- 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 →
0or0.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 generatefor 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 examplesconvert-golang-python- Reverse conversion (Go → Python)lang-python-dev- Python development patternslang-go-dev- Go development patterns
Cross-cutting pattern skills:
patterns-concurrency-dev- Async patterns, threading, goroutines across languagespatterns-serialization-dev- JSON, validation, struct tags across languagespatterns-metaprogramming-dev- Decorators, code generation across languages