Claude Code Plugins

Community-maintained marketplace

Feedback

convert-typescript-golang

@aRustyDev/ai
0
0

Convert TypeScript code to idiomatic Go. Use when migrating TypeScript projects to Go, translating TypeScript patterns to idiomatic Go, or refactoring TypeScript codebases into Go. Extends meta-convert-dev with TypeScript-to-Go specific patterns.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name convert-typescript-golang
description Convert TypeScript code to idiomatic Go. Use when migrating TypeScript projects to Go, translating TypeScript patterns to idiomatic Go, or refactoring TypeScript codebases into Go. Extends meta-convert-dev with TypeScript-to-Go specific patterns.

Convert TypeScript to Go

Convert TypeScript code to idiomatic Go. This skill extends meta-convert-dev with TypeScript-to-Go specific type mappings, idiom translations, and tooling.

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: TypeScript types → Go types
  • Idiom translations: TypeScript patterns → idiomatic Go
  • Error handling: Exceptions → error return values
  • Async patterns: Promise/async → goroutines/channels
  • Interface patterns: Structural typing → Go interfaces

This Skill Does NOT Cover

  • General conversion methodology - see meta-convert-dev
  • TypeScript language fundamentals - see lang-typescript-dev
  • Go language fundamentals - see lang-golang-dev
  • Reverse conversion (Go → TypeScript) - see convert-golang-typescript

Quick Reference

TypeScript Go Notes
string string Direct mapping
number int, int64, float64 Specify precision
boolean bool Direct mapping
null | undefined nil or zero value Go has nil for pointers/interfaces/slices/maps/channels
T[] []T Slice (dynamic array)
Array<T> []T Slice
[T, U] struct { First T; Second U } Named fields preferred over tuples
Record<K, V> map[K]V Map type
Map<K, V> map[K]V Map type
Set<T> map[T]struct{} or map[T]bool Set via map with empty struct
T | U interface{} or custom type Use type switch or discriminated union pattern
Promise<T> chan T or function with error return Goroutines for async execution
interface X type X interface Behavior contracts
class X type X struct + methods Methods with receiver syntax
enum const block with iota Or string constants
any interface{} or any (Go 1.18+) Avoid when possible
void No return value Function signature omits return
never No direct equivalent Use panic or infinite loop

When Converting Code

  1. Analyze source thoroughly before writing target
  2. Map types first - create type equivalence table
  3. Preserve semantics over syntax similarity
  4. Adopt target idioms - don't write "TypeScript code in Go syntax"
  5. Handle edge cases - null/nil/undefined, error paths, resource cleanup
  6. Test equivalence - same inputs → same outputs

Type System Mapping

Primitive Types

TypeScript Go Notes
string string UTF-8 encoded in Go
number int Default for integers without decimal
number int8, int16, int32, int64 Sized integers
number uint, uint8, uint16, uint32, uint64 Unsigned integers
number float32, float64 Floating point
bigint *big.Int From math/big package
boolean bool Direct mapping
null nil For pointers, interfaces, slices, maps, channels, functions
undefined Zero value Each type has a zero value (0, "", false, nil)
symbol No direct equivalent Use string or int constants
any interface{} or any any alias added in Go 1.18
unknown interface{} with type assertion Requires type checking
void (no return) Function returns nothing
never No direct equivalent Functions that never return use panic

Collection Types

TypeScript Go Notes
T[] []T Slice (resizable, passed by reference)
Array<T> []T Same as T[]
readonly T[] []T Go doesn't enforce readonly at compile time
[number, number, number] [3]T Fixed-size array
[T, U] struct { A T; B U } Named struct preferred
[T, U, V] struct { A T; B U; C V } Named struct preferred
Map<K, V> map[K]V Hash map
Record<K, V> map[K]V Hash map
Set<T> map[T]struct{} Empty struct uses zero memory
Set<T> map[T]bool Alternative using bool (1 byte per entry)
WeakMap No direct equivalent Use map with manual cleanup
WeakSet No direct equivalent Use map with manual cleanup

Composite Types

TypeScript Go Notes
interface X { ... } (data) type X struct { ... } Data structures
interface X { method(): T } type X interface { Method() T } Behavior contracts
class X type X struct + methods Struct with receiver methods
type X = Y type X = Y Type alias (Go 1.9+)
type X = Y | Z Custom type with methods Discriminated union pattern
T | null *T Pointer can be nil
T | undefined *T or zero value check Pointer or explicit check
Partial<T> Struct with pointer fields Each field can be nil
Required<T> Struct with value fields All fields have values
Pick<T, K> New struct type Select fields manually
Omit<T, K> New struct type Exclude fields manually
enum X const block with iota Or typed constants
namespace X package X Package organization

Generic Type Mappings

TypeScript Go Notes
<T> [T any] Generic type parameter (Go 1.18+)
<T extends U> [T U] Type constraint using interface
<T extends keyof U> No direct equivalent Use reflection or code generation
Array<T> []T Built-in generic slice
Promise<T> chan T or function return Channels for async communication
Readonly<T> No language support Convention and documentation
Record<K, V> map[K]V Built-in generic map

Idiom Translation

Pattern: Null/Undefined Handling

TypeScript:

const name = user?.name ?? "Anonymous";
const age = user?.age || 18;

Go:

var name string
if user != nil && user.Name != "" {
    name = user.Name
} else {
    name = "Anonymous"
}

age := 18
if user != nil && user.Age > 0 {
    age = user.Age
}

Why this translation:

  • Go doesn't have optional chaining or null coalescing operators
  • Explicit nil checks are idiomatic and clear
  • Zero values (0, "", false) should be considered in logic
  • Pointers are used when nil is a meaningful state

Pattern: Array/Slice Operations

TypeScript:

const activeValues = items
  .filter(x => x.active)
  .map(x => x.value)
  .reduce((sum, val) => sum + val, 0);

Go:

var activeValues int
for _, item := range items {
    if item.Active {
        activeValues += item.Value
    }
}

Why this translation:

  • Go prefers explicit for loops over chaining methods
  • More readable and maintainable for Go developers
  • Better performance (single pass, no intermediate allocations)
  • Can use generics (Go 1.18+) for reusable map/filter if needed

Pattern: Object Destructuring

TypeScript:

const { name, age, ...rest } = user;
const [first, second, ...others] = items;

Go:

name := user.Name
age := user.Age
// rest requires manual field copying or reflection

first := items[0]
second := items[1]
others := items[2:]

Why this translation:

  • Go doesn't support destructuring
  • Direct field access is clear and explicit
  • Slice operations provide array destructuring
  • Manual copying ensures clarity about what's being used

Pattern: Classes and Methods

TypeScript:

class Calculator {
  private total: number = 0;

  add(value: number): void {
    this.total += value;
  }

  getTotal(): number {
    return this.total;
  }
}

Go:

type Calculator struct {
    total int // unexported (lowercase) is private
}

func NewCalculator() *Calculator {
    return &Calculator{total: 0}
}

func (c *Calculator) Add(value int) {
    c.total += value
}

func (c *Calculator) GetTotal() int {
    return c.total
}

Why this translation:

  • Go uses struct types instead of classes
  • Methods have receiver syntax (c *Calculator)
  • Constructor pattern uses New* functions
  • Exported/unexported controlled by capitalization
  • Pointer receivers allow mutation

Pattern: Interfaces and Duck Typing

TypeScript:

interface Drawable {
  draw(): void;
}

function render(item: Drawable): void {
  item.draw();
}

// Any object with draw() method satisfies interface
const circle = { draw: () => console.log("circle") };
render(circle);

Go:

type Drawable interface {
    Draw()
}

func Render(item Drawable) {
    item.Draw()
}

// Explicit type must implement interface
type Circle struct{}

func (c Circle) Draw() {
    fmt.Println("circle")
}

// Usage
circle := Circle{}
Render(circle)

Why this translation:

  • Both languages support structural typing for interfaces
  • Go requires explicit types, not object literals
  • Interface satisfaction is implicit (no implements keyword)
  • Method names must match exactly (case-sensitive)

Pattern: Default Parameters

TypeScript:

function greet(name: string = "Guest", greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

Go:

func Greet(name, greeting string) string {
    if name == "" {
        name = "Guest"
    }
    if greeting == "" {
        greeting = "Hello"
    }
    return fmt.Sprintf("%s, %s!", greeting, name)
}

// Or use options pattern for complex cases
type GreetOptions struct {
    Name     string
    Greeting string
}

func GreetWithOptions(opts GreetOptions) string {
    if opts.Name == "" {
        opts.Name = "Guest"
    }
    if opts.Greeting == "" {
        opts.Greeting = "Hello"
    }
    return fmt.Sprintf("%s, %s!", opts.Greeting, opts.Name)
}

Why this translation:

  • Go doesn't support default parameters
  • Check zero values and provide defaults explicitly
  • Options pattern for complex parameter sets
  • Functional options pattern for even more flexibility

Pattern: Spread Operator

TypeScript:

const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5];

const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 };

Go:

arr1 := []int{1, 2, 3}
arr2 := append(append([]int{}, arr1...), 4, 5)
// Or clearer:
arr2 := make([]int, len(arr1), len(arr1)+2)
copy(arr2, arr1)
arr2 = append(arr2, 4, 5)

// No built-in object spread; must copy manually
obj2 := struct{ A, B, C int }{
    A: obj1.A,
    B: obj1.B,
    C: 3,
}

Why this translation:

  • Go uses append with ... for variadic slice expansion
  • Pre-allocating capacity avoids reallocation
  • No object spread; manual field copying required
  • Reflection can help for generic copying but adds complexity

Pattern: Optional Properties

TypeScript:

interface User {
  name: string;
  email?: string;
  age?: number;
}

Go:

type User struct {
    Name  string
    Email *string // pointer indicates optional
    Age   *int    // nil means not provided
}

// Helper to create pointer
func StringPtr(s string) *string { return &s }
func IntPtr(i int) *int { return &i }

// Usage
user := User{
    Name:  "Alice",
    Email: StringPtr("alice@example.com"),
}

Why this translation:

  • Pointers distinguish between "not provided" (nil) and "zero value"
  • Helper functions make pointer creation cleaner
  • Alternative: use zero values and a separate "set" map
  • Consider whether nil vs zero value distinction is needed

Pattern: Union Types

TypeScript:

type Result = { success: true; data: string } | { success: false; error: string };

function process(): Result {
  if (Math.random() > 0.5) {
    return { success: true, data: "OK" };
  }
  return { success: false, error: "Failed" };
}

Go:

type Result struct {
    Success bool
    Data    string // only valid if Success == true
    Error   string // only valid if Success == false
}

func Process() Result {
    if rand.Float64() > 0.5 {
        return Result{Success: true, Data: "OK"}
    }
    return Result{Success: false, Error: "Failed"}
}

// Or use interface with type assertion
type ResultSuccess struct{ Data string }
type ResultError struct{ Error string }

func Process() interface{} {
    if rand.Float64() > 0.5 {
        return ResultSuccess{Data: "OK"}
    }
    return ResultError{Error: "Failed"}
}

Why this translation:

  • Go doesn't have union types
  • Use struct with discriminator field (Success bool)
  • Interface{} with type assertion for true sum types
  • Consider if error return pattern is more idiomatic

Pattern: String Interpolation

TypeScript:

const name = "Alice";
const age = 30;
const message = `Hello, ${name}! You are ${age} years old.`;

Go:

name := "Alice"
age := 30
message := fmt.Sprintf("Hello, %s! You are %d years old.", name, age)

Why this translation:

  • Go uses fmt.Sprintf for string formatting
  • Printf-style format verbs (%s, %d, %v, etc.)
  • Type-safe at runtime, not compile-time
  • Alternative: strings.Builder for complex concatenation

Error Handling

TypeScript Exception Model → Go Error Return Model

TypeScript:

function parseConfig(path: string): Config {
  if (!fs.existsSync(path)) {
    throw new Error(`Config file not found: ${path}`);
  }

  const content = fs.readFileSync(path, 'utf-8');

  try {
    return JSON.parse(content);
  } catch (e) {
    throw new Error(`Failed to parse config: ${e.message}`);
  }
}

// Usage
try {
  const config = parseConfig("config.json");
  console.log(config);
} catch (err) {
  console.error("Error:", err.message);
}

Go:

func ParseConfig(path string) (*Config, error) {
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return nil, fmt.Errorf("config file not found: %s", path)
    }

    content, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("failed to read config: %w", err)
    }

    var config Config
    if err := json.Unmarshal(content, &config); err != nil {
        return nil, fmt.Errorf("failed to parse config: %w", err)
    }

    return &config, nil
}

// Usage
config, err := ParseConfig("config.json")
if err != nil {
    log.Printf("Error: %v", err)
    return
}
fmt.Println(config)

Why this translation:

  • Go returns errors as values, not exceptions
  • Multiple return values: (result, error)
  • fmt.Errorf with %w wraps errors (Go 1.13+)
  • errors.Is and errors.As for error checking
  • Caller checks err != nil after each call

Custom Error Types

TypeScript:

class ValidationError extends Error {
  constructor(public field: string, message: string) {
    super(message);
    this.name = "ValidationError";
  }
}

class NotFoundError extends Error {
  constructor(public resource: string) {
    super(`${resource} not found`);
    this.name = "NotFoundError";
  }
}

function validateUser(user: User): void {
  if (!user.email) {
    throw new ValidationError("email", "Email is required");
  }
}

Go:

// Custom error types implement error interface
type ValidationError struct {
    Field   string
    Message string
}

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

type NotFoundError struct {
    Resource string
}

func (e *NotFoundError) Error() string {
    return fmt.Sprintf("%s not found", e.Resource)
}

func ValidateUser(user *User) error {
    if user.Email == "" {
        return &ValidationError{Field: "email", Message: "Email is required"}
    }
    return nil
}

// Usage with type assertion
err := ValidateUser(user)
if err != nil {
    var validationErr *ValidationError
    if errors.As(err, &validationErr) {
        log.Printf("Validation failed on field: %s", validationErr.Field)
    }
}

Why this translation:

  • Go uses error interface (Error() string method)
  • Custom error types are structs with Error() method
  • errors.As for type-safe error checking
  • errors.Is for sentinel error comparison
  • Wrap errors with fmt.Errorf("%w", err) to preserve chain

Panic vs Error Returns

TypeScript:

// Exceptions for everything
function divide(a: number, b: number): number {
  if (b === 0) {
    throw new Error("division by zero");
  }
  return a / b;
}

Go:

// Error returns for expected errors
func Divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Panic only for programmer errors (bugs)
func DivideMustNotBeZero(a, b float64) float64 {
    if b == 0 {
        panic("division by zero - caller error")
    }
    return a / b
}

Why this translation:

  • Go distinguishes expected errors (return) from bugs (panic)
  • Use error returns for conditions caller should handle
  • Use panic for programmer errors / assertions
  • recover() can catch panics (similar to catch) but rarely used
  • Idiomatic Go: errors are values, not exceptions

Concurrency Patterns

Promise → Goroutines with Channels

TypeScript:

async function fetchUser(id: string): Promise<User> {
  const response = await fetch(`/users/${id}`);
  return response.json();
}

// Usage
const user = await fetchUser("123");
console.log(user);

Go:

func FetchUser(id string) (*User, error) {
    resp, err := http.Get(fmt.Sprintf("/users/%s", id))
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var user User
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        return nil, err
    }

    return &user, nil
}

// Usage (synchronous)
user, err := FetchUser("123")
if err != nil {
    log.Fatal(err)
}
fmt.Println(user)

// Or asynchronous with goroutine
func FetchUserAsync(id string) <-chan *User {
    ch := make(chan *User, 1)
    go func() {
        user, err := FetchUser(id)
        if err != nil {
            log.Printf("Error: %v", err)
            close(ch)
            return
        }
        ch <- user
    }()
    return ch
}

// Usage
userChan := FetchUserAsync("123")
user := <-userChan // Wait for result

Why this translation:

  • Go uses goroutines (lightweight threads) for concurrency
  • Channels communicate between goroutines
  • Synchronous by default; explicit goroutines for async
  • defer ensures cleanup (like finally)
  • Error handling remains explicit

Promise.all → WaitGroup or Channels

TypeScript:

const [users, posts, comments] = await Promise.all([
  fetchUsers(),
  fetchPosts(),
  fetchComments()
]);

Go:

// Using WaitGroup
var wg sync.WaitGroup
var users []User
var posts []Post
var comments []Comment
var mu sync.Mutex // Protect shared state if needed
var errs []error

wg.Add(3)

go func() {
    defer wg.Done()
    u, err := FetchUsers()
    if err != nil {
        mu.Lock()
        errs = append(errs, err)
        mu.Unlock()
        return
    }
    mu.Lock()
    users = u
    mu.Unlock()
}()

go func() {
    defer wg.Done()
    p, err := FetchPosts()
    if err != nil {
        mu.Lock()
        errs = append(errs, err)
        mu.Unlock()
        return
    }
    mu.Lock()
    posts = p
    mu.Unlock()
}()

go func() {
    defer wg.Done()
    c, err := FetchComments()
    if err != nil {
        mu.Lock()
        errs = append(errs, err)
        mu.Unlock()
        return
    }
    mu.Lock()
    comments = c
    mu.Unlock()
}()

wg.Wait()

if len(errs) > 0 {
    // Handle errors
}

// Or using channels (cleaner)
type Result struct {
    Users    []User
    Posts    []Post
    Comments []Comment
}

func FetchAll() (*Result, error) {
    usersCh := make(chan []User, 1)
    postsCh := make(chan []Post, 1)
    commentsCh := make(chan []Comment, 1)
    errCh := make(chan error, 3)

    go func() {
        users, err := FetchUsers()
        if err != nil {
            errCh <- err
            return
        }
        usersCh <- users
    }()

    go func() {
        posts, err := FetchPosts()
        if err != nil {
            errCh <- err
            return
        }
        postsCh <- posts
    }()

    go func() {
        comments, err := FetchComments()
        if err != nil {
            errCh <- err
            return
        }
        commentsCh <- comments
    }()

    result := &Result{}
    for i := 0; i < 3; i++ {
        select {
        case users := <-usersCh:
            result.Users = users
        case posts := <-postsCh:
            result.Posts = posts
        case comments := <-commentsCh:
            result.Comments = comments
        case err := <-errCh:
            return nil, err
        }
    }

    return result, nil
}

Why this translation:

  • sync.WaitGroup waits for multiple goroutines
  • Channels communicate results between goroutines
  • select statement waits on multiple channels
  • Explicit error handling for each operation
  • Consider errgroup package for cleaner error handling

Async/Await → Goroutines

TypeScript:

async function processItems(items: string[]): Promise<string[]> {
  const results: string[] = [];

  for (const item of items) {
    const result = await processItem(item);
    results.push(result);
  }

  return results;
}

Go:

// Sequential (like await)
func ProcessItems(items []string) ([]string, error) {
    results := make([]string, 0, len(items))

    for _, item := range items {
        result, err := ProcessItem(item)
        if err != nil {
            return nil, err
        }
        results = append(results, result)
    }

    return results, nil
}

// Concurrent (parallel processing)
func ProcessItemsConcurrent(items []string) ([]string, error) {
    results := make([]string, len(items))
    errCh := make(chan error, len(items))
    var wg sync.WaitGroup

    for i, item := range items {
        wg.Add(1)
        go func(index int, item string) {
            defer wg.Done()
            result, err := ProcessItem(item)
            if err != nil {
                errCh <- err
                return
            }
            results[index] = result
        }(i, item)
    }

    wg.Wait()
    close(errCh)

    if len(errCh) > 0 {
        return nil, <-errCh
    }

    return results, nil
}

Why this translation:

  • Go doesn't have async/await keywords
  • Sequential code is default (like synchronous)
  • Use goroutines explicitly for concurrency
  • Channels or WaitGroup coordinate goroutines
  • More control over concurrency patterns

Event Emitters → Channels

TypeScript:

import { EventEmitter } from 'events';

const emitter = new EventEmitter();

emitter.on('data', (value: number) => {
  console.log('Received:', value);
});

emitter.emit('data', 42);

Go:

// Channel-based pub/sub
type EventBus struct {
    subscribers []chan int
    mu          sync.RWMutex
}

func NewEventBus() *EventBus {
    return &EventBus{
        subscribers: make([]chan int, 0),
    }
}

func (eb *EventBus) Subscribe() <-chan int {
    eb.mu.Lock()
    defer eb.mu.Unlock()

    ch := make(chan int, 10) // buffered
    eb.subscribers = append(eb.subscribers, ch)
    return ch
}

func (eb *EventBus) Publish(value int) {
    eb.mu.RLock()
    defer eb.mu.RUnlock()

    for _, ch := range eb.subscribers {
        ch <- value
    }
}

// Usage
bus := NewEventBus()
dataCh := bus.Subscribe()

go func() {
    for value := range dataCh {
        fmt.Println("Received:", value)
    }
}()

bus.Publish(42)

Why this translation:

  • Go uses channels for message passing
  • Explicit subscriber management
  • Type-safe channels (type per channel)
  • Consider existing packages (e.g., EventBus libraries)
  • Goroutines handle async listeners

Common Pitfalls

1. Ignoring Error Returns

Problem:

// ❌ Ignoring error return
user, _ := FetchUser("123")
fmt.Println(user.Name) // Potential nil pointer dereference!

Solution:

// ✓ Always check errors
user, err := FetchUser("123")
if err != nil {
    return fmt.Errorf("failed to fetch user: %w", err)
}
fmt.Println(user.Name)

Why:

  • Go conventions require explicit error handling
  • Ignoring errors leads to runtime panics
  • Use linters (errcheck, golangci-lint) to catch

2. Using Pointers When Not Needed

Problem:

// ❌ Unnecessary pointer
func Double(n *int) *int {
    result := *n * 2
    return &result
}

Solution:

// ✓ Pass by value for small types
func Double(n int) int {
    return n * 2
}

Why:

  • Small types (int, bool, small structs) are efficient by value
  • Pointers add indirection and nil checks
  • Use pointers for: large structs, mutation, or optional values

3. Modifying Loop Variables in Goroutines

Problem:

// ❌ Loop variable capture bug
for _, item := range items {
    go func() {
        process(item) // All goroutines see last item!
    }()
}

Solution:

// ✓ Pass variable as parameter or shadow it
for _, item := range items {
    item := item // shadow
    go func() {
        process(item)
    }()
}

// Or pass as parameter
for _, item := range items {
    go func(i string) {
        process(i)
    }(item)
}

Why:

  • Loop variable is reused across iterations
  • Goroutines capture variable reference, not value
  • Fixed in Go 1.22+ with per-iteration variables

4. Not Closing Channels

Problem:

// ❌ Channel never closed
ch := make(chan int)
go func() {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    // Never closes!
}()

for val := range ch {
    fmt.Println(val) // Hangs after 10 values
}

Solution:

// ✓ Close channel when done
ch := make(chan int)
go func() {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}()

for val := range ch {
    fmt.Println(val)
}

Why:

  • range on channel blocks until closed
  • close() signals no more values coming
  • Only sender should close (not receiver)

5. Misunderstanding Zero Values

Problem:

// TypeScript: undefined check
if (user.age !== undefined) {
  // age was explicitly set
}
// ❌ Go: can't distinguish zero value from explicit zero
if user.Age != 0 {
    // Could be unset OR explicitly set to 0!
}

Solution:

// ✓ Use pointers for optional values
type User struct {
    Name string
    Age  *int // nil means not set, 0 means explicitly zero
}

if user.Age != nil {
    fmt.Println(*user.Age)
}

Why:

  • Go initializes all variables to zero values
  • Can't distinguish "not set" from "set to zero"
  • Use pointers when distinction matters

6. Forgetting defer for Cleanup

Problem:

// ❌ Manual cleanup easy to forget
file, err := os.Open("file.txt")
if err != nil {
    return err
}
// ... lots of code ...
if someError {
    return someError // Forgot to close file!
}
file.Close()

Solution:

// ✓ defer ensures cleanup
file, err := os.Open("file.txt")
if err != nil {
    return err
}
defer file.Close() // Always runs before function returns

// ... code can return anywhere ...

Why:

  • defer guarantees cleanup on all return paths
  • Executes in LIFO order
  • Common for: files, mutexes, database connections

7. Copying Mutexes

Problem:

// ❌ Copying struct with mutex
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c Counter) Inc() { // Value receiver copies mutex!
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

Solution:

// ✓ Pointer receiver for structs with mutexes
func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
}

Why:

  • Copying a locked mutex is undefined behavior
  • sync types (Mutex, WaitGroup, etc.) must not be copied
  • Use pointer receivers for types with sync primitives

8. Interface nil Confusion

Problem:

// ❌ Interface containing nil pointer isn't nil!
var p *User = nil
var i interface{} = p
if i != nil {
    fmt.Println("Not nil!") // This prints!
}

Solution:

// ✓ Check for nil before assigning to interface
var p *User = nil
if p != nil {
    i = p
} else {
    i = nil // Or don't assign
}

// Or use typed nil check
var i interface{} = (*User)(nil)
if i == nil || i.(*User) == nil {
    // Actually nil
}

Why:

  • Interface stores (type, value) pair
  • (type=*User, value=nil) != (type=nil, value=nil)
  • Common source of bugs in error returns

Tooling

Conversion Tools

Tool Purpose Notes
Manual TypeScript → Go No mature automated converter exists
AST analysis Parse TypeScript Use TypeScript compiler API
Code generation Generate Go Template-based or custom tooling

Go Development Tools

Tool Purpose Notes
go fmt Code formatting Standardizes formatting
goimports Import management Adds/removes imports automatically
golangci-lint Linting Comprehensive linter aggregator
go vet Static analysis Detects common mistakes
staticcheck Advanced static analysis High-quality checks
errcheck Error checking Ensures errors are checked
go test Testing Built-in test framework
go test -race Race detection Detects race conditions
pprof Profiling CPU and memory profiling
delve Debugging Go debugger

Testing Tools

Tool Purpose Notes
testing package Unit tests Built-in framework
testify Assertions Popular testing toolkit
gomock Mocking Interface mocking
httptest HTTP testing Test HTTP handlers
go-cmp Deep comparison Compare complex structures

Dependency Management

TypeScript (npm) Go Equivalent Notes
axios net/http, resty HTTP clients
lodash Built-ins Go has many built-in utilities
express net/http, gin, echo Web frameworks
jest testing Built-in testing
dotenv godotenv, viper Config management
winston log, zap, logrus Logging
commander flag, cobra, urfave/cli CLI parsing
date-fns time Date/time built-in
uuid google/uuid UUID generation
zod validator, ozzo-validation Validation

Examples

Example 1: Simple - Type and Function Translation

Before (TypeScript):

interface User {
  id: string;
  name: string;
  age: number;
  email?: string;
}

function findUserByID(users: User[], id: string): User | undefined {
  return users.find(u => u.id === id);
}

// Usage
const users: User[] = [
  { id: "1", name: "Alice", age: 30, email: "alice@example.com" },
  { id: "2", name: "Bob", age: 25 },
];

const user = findUserByID(users, "1");
if (user) {
  console.log(`Found: ${user.name}`);
}

After (Go):

package main

import "fmt"

type User struct {
    ID    string
    Name  string
    Age   int
    Email *string // pointer for optional field
}

func FindUserByID(users []User, id string) *User {
    for i := range users {
        if users[i].ID == id {
            return &users[i]
        }
    }
    return nil
}

func main() {
    email := "alice@example.com"
    users := []User{
        {ID: "1", Name: "Alice", Age: 30, Email: &email},
        {ID: "2", Name: "Bob", Age: 25, Email: nil},
    }

    user := FindUserByID(users, "1")
    if user != nil {
        fmt.Printf("Found: %s\n", user.Name)
    }
}

Example 2: Medium - Error Handling and JSON

Before (TypeScript):

import * as fs from 'fs';

interface Config {
  host: string;
  port: number;
  debug: boolean;
}

class ConfigError extends Error {
  constructor(message: string) {
    super(message);
    this.name = "ConfigError";
  }
}

function loadConfig(path: string): Config {
  if (!fs.existsSync(path)) {
    throw new ConfigError(`Config file not found: ${path}`);
  }

  const content = fs.readFileSync(path, 'utf-8');

  try {
    const config = JSON.parse(content);

    if (!config.host || typeof config.port !== 'number') {
      throw new ConfigError("Invalid config format");
    }

    return config as Config;
  } catch (e) {
    if (e instanceof ConfigError) {
      throw e;
    }
    throw new ConfigError(`Failed to parse config: ${(e as Error).message}`);
  }
}

// Usage
try {
  const config = loadConfig("config.json");
  console.log(`Server running on ${config.host}:${config.port}`);
} catch (err) {
  if (err instanceof ConfigError) {
    console.error("Config error:", err.message);
    process.exit(1);
  }
}

After (Go):

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Config struct {
    Host  string `json:"host"`
    Port  int    `json:"port"`
    Debug bool   `json:"debug"`
}

type ConfigError struct {
    Message string
}

func (e *ConfigError) Error() string {
    return e.Message
}

func LoadConfig(path string) (*Config, error) {
    if _, err := os.Stat(path); os.IsNotExist(err) {
        return nil, &ConfigError{
            Message: fmt.Sprintf("config file not found: %s", path),
        }
    }

    content, err := os.ReadFile(path)
    if err != nil {
        return nil, &ConfigError{
            Message: fmt.Sprintf("failed to read config: %v", err),
        }
    }

    var config Config
    if err := json.Unmarshal(content, &config); err != nil {
        return nil, &ConfigError{
            Message: fmt.Sprintf("failed to parse config: %v", err),
        }
    }

    if config.Host == "" || config.Port == 0 {
        return nil, &ConfigError{
            Message: "invalid config format",
        }
    }

    return &config, nil
}

func main() {
    config, err := LoadConfig("config.json")
    if err != nil {
        var configErr *ConfigError
        if errors.As(err, &configErr) {
            fmt.Fprintf(os.Stderr, "Config error: %s\n", configErr.Message)
            os.Exit(1)
        }
    }

    fmt.Printf("Server running on %s:%d\n", config.Host, config.Port)
}

Example 3: Complex - HTTP API with Async Operations

Before (TypeScript):

import axios from 'axios';
import { EventEmitter } from 'events';

interface User {
  id: string;
  name: string;
  email: string;
}

interface Post {
  id: string;
  userId: string;
  title: string;
  content: string;
}

class APIClient extends EventEmitter {
  private baseURL: string;
  private cache: Map<string, any> = new Map();

  constructor(baseURL: string) {
    super();
    this.baseURL = baseURL;
  }

  async fetchUser(id: string): Promise<User> {
    const cacheKey = `user:${id}`;

    if (this.cache.has(cacheKey)) {
      this.emit('cache-hit', cacheKey);
      return this.cache.get(cacheKey);
    }

    try {
      const response = await axios.get<User>(`${this.baseURL}/users/${id}`);
      this.cache.set(cacheKey, response.data);
      this.emit('user-fetched', response.data);
      return response.data;
    } catch (error) {
      this.emit('error', error);
      throw new Error(`Failed to fetch user ${id}: ${error.message}`);
    }
  }

  async fetchUserPosts(userId: string): Promise<Post[]> {
    try {
      const response = await axios.get<Post[]>(
        `${this.baseURL}/users/${userId}/posts`
      );
      return response.data;
    } catch (error) {
      throw new Error(`Failed to fetch posts: ${error.message}`);
    }
  }

  async getUserWithPosts(userId: string): Promise<{ user: User; posts: Post[] }> {
    // Fetch in parallel
    const [user, posts] = await Promise.all([
      this.fetchUser(userId),
      this.fetchUserPosts(userId),
    ]);

    return { user, posts };
  }

  clearCache(): void {
    this.cache.clear();
    this.emit('cache-cleared');
  }
}

// Usage
async function main() {
  const client = new APIClient('https://api.example.com');

  client.on('user-fetched', (user: User) => {
    console.log('Fetched user:', user.name);
  });

  client.on('cache-hit', (key: string) => {
    console.log('Cache hit:', key);
  });

  client.on('error', (error: Error) => {
    console.error('API error:', error.message);
  });

  try {
    const result = await client.getUserWithPosts('123');
    console.log(`${result.user.name} has ${result.posts.length} posts`);

    // Second fetch will hit cache
    await client.fetchUser('123');
  } catch (error) {
    console.error('Failed:', error.message);
    process.exit(1);
  }
}

main();

After (Go):

package main

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

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

type Post struct {
    ID      string `json:"id"`
    UserID  string `json:"userId"`
    Title   string `json:"title"`
    Content string `json:"content"`
}

type Event struct {
    Type string
    Data interface{}
}

type APIClient struct {
    baseURL     string
    cache       map[string]interface{}
    cacheMu     sync.RWMutex
    eventCh     chan Event
    httpClient  *http.Client
}

func NewAPIClient(baseURL string) *APIClient {
    return &APIClient{
        baseURL:    baseURL,
        cache:      make(map[string]interface{}),
        eventCh:    make(chan Event, 100),
        httpClient: &http.Client{},
    }
}

func (c *APIClient) Events() <-chan Event {
    return c.eventCh
}

func (c *APIClient) emit(eventType string, data interface{}) {
    select {
    case c.eventCh <- Event{Type: eventType, Data: data}:
    default:
        // Don't block if no listeners
    }
}

func (c *APIClient) FetchUser(id string) (*User, error) {
    cacheKey := fmt.Sprintf("user:%s", id)

    // Check cache
    c.cacheMu.RLock()
    if cached, ok := c.cache[cacheKey]; ok {
        c.cacheMu.RUnlock()
        c.emit("cache-hit", cacheKey)
        return cached.(*User), nil
    }
    c.cacheMu.RUnlock()

    // Fetch from API
    url := fmt.Sprintf("%s/users/%s", c.baseURL, id)
    resp, err := c.httpClient.Get(url)
    if err != nil {
        c.emit("error", err)
        return nil, fmt.Errorf("failed to fetch user %s: %w", id, err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        err := fmt.Errorf("API returned status %d", resp.StatusCode)
        c.emit("error", err)
        return nil, err
    }

    var user User
    if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
        c.emit("error", err)
        return nil, fmt.Errorf("failed to decode user: %w", err)
    }

    // Update cache
    c.cacheMu.Lock()
    c.cache[cacheKey] = &user
    c.cacheMu.Unlock()

    c.emit("user-fetched", &user)
    return &user, nil
}

func (c *APIClient) FetchUserPosts(userID string) ([]Post, error) {
    url := fmt.Sprintf("%s/users/%s/posts", c.baseURL, userID)
    resp, err := c.httpClient.Get(url)
    if err != nil {
        return nil, fmt.Errorf("failed to fetch posts: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
    }

    var posts []Post
    if err := json.NewDecoder(resp.Body).Decode(&posts); err != nil {
        return nil, fmt.Errorf("failed to decode posts: %w", err)
    }

    return posts, nil
}

type UserWithPosts struct {
    User  *User
    Posts []Post
}

func (c *APIClient) GetUserWithPosts(userID string) (*UserWithPosts, error) {
    // Fetch in parallel using goroutines
    type userResult struct {
        user *User
        err  error
    }
    type postsResult struct {
        posts []Post
        err   error
    }

    userCh := make(chan userResult, 1)
    postsCh := make(chan postsResult, 1)

    go func() {
        user, err := c.FetchUser(userID)
        userCh <- userResult{user, err}
    }()

    go func() {
        posts, err := c.FetchUserPosts(userID)
        postsCh <- postsResult{posts, err}
    }()

    // Wait for both
    userRes := <-userCh
    postsRes := <-postsCh

    if userRes.err != nil {
        return nil, userRes.err
    }
    if postsRes.err != nil {
        return nil, postsRes.err
    }

    return &UserWithPosts{
        User:  userRes.user,
        Posts: postsRes.posts,
    }, nil
}

func (c *APIClient) ClearCache() {
    c.cacheMu.Lock()
    defer c.cacheMu.Unlock()

    c.cache = make(map[string]interface{})
    c.emit("cache-cleared", nil)
}

func main() {
    client := NewAPIClient("https://api.example.com")

    // Event listener goroutine
    go func() {
        for event := range client.Events() {
            switch event.Type {
            case "user-fetched":
                user := event.Data.(*User)
                fmt.Println("Fetched user:", user.Name)
            case "cache-hit":
                key := event.Data.(string)
                fmt.Println("Cache hit:", key)
            case "error":
                err := event.Data.(error)
                fmt.Println("API error:", err)
            case "cache-cleared":
                fmt.Println("Cache cleared")
            }
        }
    }()

    result, err := client.GetUserWithPosts("123")
    if err != nil {
        fmt.Fprintf(os.Stderr, "Failed: %v\n", err)
        os.Exit(1)
    }

    fmt.Printf("%s has %d posts\n", result.User.Name, len(result.Posts))

    // Second fetch will hit cache
    _, _ = client.FetchUser("123")
}

See Also

For more examples and patterns, see:

  • meta-convert-dev - Foundational patterns with cross-language examples
  • lang-typescript-dev - TypeScript development patterns
  • lang-golang-dev - Go development patterns