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.
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
varkeyword in Python; Go usesvaror:= - Go uses
PascalCasefor exported names,camelCasefor 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
&¬and,||notor - 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
implementskeyword) - 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
- CLI Tool: Rewrite a Python script in Go
- API Server: Build a REST API with stdlib or Gin
- Worker: Background job processor with goroutines
- 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.