Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungTCP vs UDP: The Two Ways to Move Data, and Why Neither Is "Better"(21.09.2026 um 09:31 Uhr)
Sichere ProgrammierungBukan Sekadar Variabel, Tapi Nyawa dari Aplikasi Kamu! 🚀(21.09.2026 um 09:36 Uhr)
Sichere ProgrammierungAI voice agent for customer service: what stops callers hanging up?(21.09.2026 um 09:42 Uhr)
Sichere ProgrammierungReading a small model's confidence instead of its prose(21.09.2026 um 09:47 Uhr)
Sichere ProgrammierungTCP vs UDP: The Two Ways to Move Data, and Why Neither Is "Better"(21.09.2026 um 09:31 Uhr)
Sichere ProgrammierungBukan Sekadar Variabel, Tapi Nyawa dari Aplikasi Kamu! 🚀(21.09.2026 um 09:36 Uhr)
Sichere ProgrammierungAI voice agent for customer service: what stops callers hanging up?(21.09.2026 um 09:42 Uhr)
Sichere ProgrammierungReading a small model's confidence instead of its prose(21.09.2026 um 09:47 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Go's Implicit Interfaces Are Its Best Architecture Feature

Book: Hexagonal Architecture in Go Also by me: Thinking in Go (2-book series) — Complete Guide to Go Programming + Hexagonal Architecture in Go My project: Hermes IDE | GitHub — an IDE for developers who ship with Claude Code and other …

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!





You hear it every time someone pitches Go. "Goroutines are amazing." "Channels change the way you think about concurrency." "The scheduler is so lightweight you can spin up 100k goroutines on a laptop."



All true. None of it is what makes Go special for building well-structured software.



The feature that actually changes how you design systems is implicit interface satisfaction. It is the reason hexagonal architecture in Go feels like a natural extension of the language rather than a pattern you bolt on with a framework and a prayer.






What Implicit Satisfaction Actually Means



In Java, you declare your allegiance. A class that implements an interface says so explicitly:




public class PostgresOrderRepo
implements OrderRepository {

@Override
public Order findById(String id) {
// SQL query
}

@Override
public void save(Order order) {
// SQL insert
}
}






The implementation imports and names the interface. It is coupled to the interface at the source level. If OrderRepository lives in a different module, you need that module on the classpath at compile time.



In Go, there is no implements keyword. A type satisfies an interface by having the right methods. That is it.




// In your domain package
type OrderRepository interface {
FindByID(
ctx context.Context,
id string,
) (Order, error)
Save(
ctx context.Context,
order Order,
) error
}









// In your infrastructure package — a completely
// separate directory, separate import path
type PostgresOrderRepo struct {
db *sql.DB
}

func (r *PostgresOrderRepo) FindByID(
ctx context.Context,
id string,
) (Order, error) {
// actual SQL query
return Order{}, nil
}

func (r *PostgresOrderRepo) Save(
ctx context.Context,
order Order,
) error {
// actual SQL insert
return nil
}






PostgresOrderRepo never mentions OrderRepository. It imports the domain types it persists but has no dependency on the port interface. The compiler will accept it anywhere an OrderRepository is expected without the adapter ever declaring that relationship.



This is not a minor syntactic convenience. It inverts the direction of the dependency.






Why This Matters for Hexagonal Architecture



Hexagonal architecture (ports and adapters) has one core rule: dependencies point inward. Your domain logic sits at the center. Infrastructure (databases, message queues, HTTP handlers) lives at the edges. The domain defines what it needs through ports (interfaces). Adapters implement those ports.



In Java, implementing this rule takes work. The adapter must import the port interface. That import creates a compile-time dependency from the outer ring to the inner ring, which is what you want. But it also means the adapter has to explicitly declare implements OrderRepository. Rename the interface and you rename every adapter; move it to a different package and every import updates. Add a method and every adapter fails to compile until you add the stub.



This is manageable for a single service. Scale it to a codebase with 20 ports and 40 adapters, and you start reaching for code generation, DI containers like Spring or Guice, and annotation processors.



In Go, none of that ceremony exists.



Your domain package defines the port:




package order

type Repository interface {
FindByID(
ctx context.Context,
id string,
) (Order, error)
Save(ctx context.Context, o Order) error
}

type Service struct {
repo Repository
}

func NewService(r Repository) *Service {
return &Service{repo: r}
}

func (s *Service) Place(
ctx context.Context,
items []Item,
) (Order, error) {
o := NewOrder(items)
if err := s.repo.Save(ctx, o); err != nil {
return Order{}, fmt.Errorf(
"saving order: %w", err,
)
}
return o, nil
}






Your Postgres adapter sits in a separate package. It imports order for the domain types it persists, but it never references order.Repository. The interface does not appear in the adapter's source at all:




package postgres

type OrderRepo struct {
db *sql.DB
}

func (r *OrderRepo) FindByID(
ctx context.Context,
id string,
) (order.Order, error) {
row := r.db.QueryRowContext(
ctx,
`SELECT id, status, total
FROM orders WHERE id = $1`
,
id,
)
var o order.Order
err := row.Scan(&o.ID, &o.Status, &o.Total)
if err != nil {
return order.Order{}, fmt.Errorf(
"finding order %s: %w", id, err,
)
}
return o, nil
}






The Save method follows the same pattern. It works with order.Order values but has zero knowledge of the port interface:




func (r *OrderRepo) Save(
ctx context.Context,
o order.Order,
) error {
_, err := r.db.ExecContext(
ctx,
`INSERT INTO orders (id, status, total)
VALUES ($1, $2, $3)`
,
o.ID, o.Status, o.Total,
)
if err != nil {
return fmt.Errorf(
"saving order %s: %w", o.ID, err,
)
}
return nil
}






The wiring happens in main, the only place that knows about both packages:




func main() {
db := connectDB()
repo := &postgres.OrderRepo{DB: db}
svc := order.NewService(repo)
// ... start HTTP server, pass svc to handlers
}






No DI container. No annotation scanning. No wire or fx unless you want them. The compiler does the verification for free. If postgres.OrderRepo is missing a method, the assignment order.NewService(repo) fails at compile time with a clear message.






Consumer-Side Interface Ownership



Implicit satisfaction enables something that explicit implements makes awkward: the consumer defines the interface, not the provider.



This is a well-known Go proverb ("accept interfaces, return structs"), but the architectural consequence is underappreciated. When the consumer owns the interface, it declares only the methods it needs.



Say your order service needs to look up users:




package order

type UserLookup interface {
ByID(
ctx context.Context,
id string,
) (User, error)
}

type Service struct {
repo Repository
users UserLookup
}






The user package might have a struct with 15 methods — Create, Delete, ListByOrg, ResetPassword, VerifyEmail. The order service does not care. It defines a one-method interface. The user struct satisfies it without knowing.



In Java, you would need either a separate UserLookup interface in the user package (which means coordinating two packages) or a shared interface module (which means a third package). In Go, the consumer writes what it needs and moves on.



This has a concrete payoff for testing. Your test double for UserLookup is four lines:




type stubUsers struct {
user order.User
err error
}

func (s *stubUsers) ByID(
_ context.Context,
_ string,
) (order.User, error) {
return s.user, s.err
}






You skip the mocking framework entirely. No codegen. No 15-method stub where 14 methods panic with "not implemented."






The Adapter Pattern Without a Framework



Consider what happens when you need to swap your messaging backend from RabbitMQ to Kafka. In a Java codebase with Spring, you are likely changing bean configurations, updating dependency injection bindings, maybe writing an adapter class with @Component and @Qualifier annotations.



In Go, your domain defines what it needs:




package notification

type Publisher interface {
Publish(
ctx context.Context,
event Event,
) error
}






Your RabbitMQ adapter:




package rabbitmq

type Publisher struct {
ch *amqp.Channel
}

func (p *Publisher) Publish(
ctx context.Context,
event notification.Event,
) error {
body, err := json.Marshal(event)
if err != nil {
return fmt.Errorf(
"marshaling event: %w", err,
)
}
return p.ch.PublishWithContext(
ctx, "events", "", false, false,
amqp.Publishing{Body: body},
)
}






Your Kafka adapter:




package kafka

type Publisher struct {
writer *kafka.Writer
}

func (p *Publisher) Publish(
ctx context.Context,
event notification.Event,
) error {
body, err := json.Marshal(event)
if err != nil {
return fmt.Errorf(
"marshaling event: %w", err,
)
}
return p.writer.WriteMessages(
ctx,
kafka.Message{Value: body},
)
}






Swapping them is a one-line change in main.go:




// Before
pub := &rabbitmq.Publisher{Ch: ch}

// After
pub := &kafka.Publisher{Writer: w}






Both satisfy notification.Publisher. Neither imports the other. Neither knows the other exists. The domain package has zero awareness of which message broker is running in production. That is the adapter pattern as described in every architecture textbook, except here you did not need a single framework to make it work.






Compile-Time Port Verification



A common worry with implicit interfaces: "How do I know my adapter actually satisfies the port? There is no explicit declaration."



The compiler tells you. But if you want a belt-and-suspenders check at the declaration site, Go has a well-established idiom:




package postgres

// Compile-time proof that OrderRepo
// satisfies order.Repository.
var _ order.Repository = (*OrderRepo)(nil)






This line allocates nothing at runtime. It is a zero-cost assertion that the compiler evaluates during build. If OrderRepo drifts out of sync with order.Repository (a renamed method, a changed signature), the build breaks immediately with a message that names the missing method.



Some teams add these assertions to every adapter file by convention. Others skip them and rely on the wiring in main to catch mismatches. Either approach works. The point is that implicit does not mean unchecked.






What About Large Interfaces?



A question that comes up often: "My port has 8 methods. Isn't that unwieldy with implicit satisfaction?"



Yes. But that is not a problem with implicit interfaces — it is a signal that your port is too wide.



The Go proverb applies: "The bigger the interface, the weaker the abstraction." If your OrderRepository has 8 methods, ask which callers need all 8. Usually, one service needs Save and FindByID. Another needs ListByCustomer. A third needs Delete.



Split the port:




type OrderWriter interface {
Save(ctx context.Context, o Order) error
}

type OrderReader interface {
FindByID(
ctx context.Context,
id string,
) (Order, error)
ListByCustomer(
ctx context.Context,
customerID string,
) ([]Order, error)
}

type OrderDeleter interface {
Delete(
ctx context.Context,
id string,
) error
}






Each service depends on the narrowest interface it needs. Your Postgres adapter still implements all the methods — it satisfies all three interfaces without declaring any of them.



Compose when a service genuinely needs the full set:




type OrderStore interface {
OrderWriter
OrderReader
OrderDeleter
}






Interface embedding gives you union types for free. And because satisfaction is implicit, the adapter does not change at all when you split or compose interfaces on the consumer side.






The Comparison, Summarized
















































Concern Java (explicit) Go (implicit)
Adapter imports interface Yes — compile-time coupling No — zero-import satisfaction
Interface ownership Provider-side (convention) Consumer-side (idiomatic)
Adding a new adapter New class + implements + DI binding New struct + matching methods
Swapping adapters DI config change + possible annotations One-line change in main
Splitting an interface Every adapter updates its implements clause Adapters untouched — they already have the methods
Verification Explicit at declaration Implicit at use-site (or var _ assertion)
Framework required for DI Practically yes (Spring, Guice, Dagger) No — constructor injection in main





Where the Conversation Gets Real



Goroutines solve a runtime problem: how to handle concurrent work cheaply. Implicit interfaces solve a different problem, one about design time. They let you structure a codebase so that changing one part does not cascade into 15 others. They are the reason you can practice hexagonal architecture in Go with nothing but the standard toolchain. No container, no framework, no build plugin.



The next time someone asks you what makes Go different, try this: it is one of very few statically typed languages where you can define a contract in one package and fulfill it in another without either package knowing about the other. That is not a party trick. That is the foundation of clean architecture in Go.









If you want to go deeper



This post scratched the surface. The full treatment covers the ground you would expect: port design conventions, interface splitting vs. composition, the IDGenerator pattern for deterministic testing, and production adapter patterns for databases, queues, and HTTP clients. It is all in my book Hexagonal Architecture in Go. If you are building Go services and want an architecture that scales with your team instead of against it, that is where to start.



The companion book, The Complete Guide to Go Programming, handles the language-level foundations that make all of this work: the type system, the module system, error handling patterns, and the concurrency model you hear so much about.



Thinking in Go — the 2-book series on Go programming and hexagonal architecture

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Go's Implicit Interfaces Are Its Best Architecture Feature

Thematisch verwandte Begriffe: Implicit, Interfaces, Best, Architecture · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-94030 | A security vulnerability has been detected in SerenityOS up to 3d83e4509…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick