03open 25 min

Structs and interfaces: simpler modeling than JavaScript classes

Model data and behavior in Go without carrying over JavaScript class instincts too literally.

by the end of this lesson you can

  • Uses a struct to hold dependencies
  • Defines only the interface behavior the consumer actually needs
  • Avoids a direct one-for-one class translation

Overview

JavaScript gives you objects, prototypes, classes, and structural flexibility. Go gives you structs for data and interfaces for behavior, but it expects you to keep both smaller and more explicit.

In JavaScript/Node, you often

mix data and behavior through classes or object literals and rely on flexible object shape for many abstractions.

In Go, the common pattern is

to use structs for concrete data and narrow interfaces only where behavior contracts are genuinely needed.

why this difference matters

This helps Go code stay flatter and easier to reason about than a direct port of JavaScript class habits.

JavaScript/Node

class User {
  constructor(name) {
    this.name = name;
  }

  greet() {
    return `hello ${this.name}`;
  }
}

Go

type User struct {
    Name string
}

func (u User) Greet() string {
    return "hello " + u.Name
}

Deeper comparison

JavaScript/Node version

class Mailer {
  send(message) {}
}

class Notifier {
  constructor(mailer) {
    this.mailer = mailer;
  }

  notify(message) {
    return this.mailer.send(message);
  }
}

Go version

type Mailer interface {
    Send(message string) error
}

type Notifier struct {
    mailer Mailer
}

func (n Notifier) Notify(message string) error {
    return n.mailer.Send(message)
}

Reflect

What gets simpler when you stop carrying JavaScript class habits directly into Go?

what a strong answer notices

A strong answer points to smaller interfaces, flatter data models, and less abstraction built only for flexibility's sake.

Rewrite

Rewrite this class-oriented JavaScript pattern into Go using a struct plus a narrow interface.

Rewrite this JavaScript/Node

class Cache {
  get(key) {}
}

class UserService {
  constructor(cache) {
    this.cache = cache;
  }

  load(id) {
    return this.cache.get(id);
  }
}

what good looks like

  • Uses a struct to hold dependencies
  • Defines only the interface behavior the consumer actually needs
  • Avoids a direct one-for-one class translation

Practice

Design a Go service that depends on a storage implementation without reproducing a JavaScript-style class hierarchy.

success criteria

  • Separates data and behavior clearly
  • Uses a narrow interface at the consumer boundary
  • Keeps the abstraction focused on the actual use case

Common mistakes

  • Porting JavaScript classes one-to-one into Go.
  • Creating broad interfaces too early because JavaScript code was structurally flexible.
  • Adding methods to structs just because the original object had them.

takeaways

  • This helps Go code stay flatter and easier to reason about than a direct port of JavaScript class habits.
  • A strong answer points to smaller interfaces, flatter data models, and less abstraction built only for flexibility's sake.
  • Separates data and behavior clearly