---
id: ebf46089667a6469b258b8e52e57571f
author: Yunjin Lee
title: 'Go-interfaces en structuurembedding: verschillen met klasse-overerving en compositieontwerp'
description: Dit document vergelijkt het verschil tussen klasse-overerving in Java/C++ en het structurele typeringsmechanisme van Go-interfaces, en beschrijft het compositiepatroon met behulp van struct-embedding en methodedelegatie aan de hand van codevoorbeelden.
language: nl
date: 2025-07-17T10:40:02.284435348Z
path: /blog/posts/go-interfaces-are-not-inheritance-zd04c1084
category: language
---

# Overzicht

Go-interfaces maken het gemakkelijk om functies met dezelfde argumenten en retourwaarden over meerdere structs heen te hebben, maar dit verschilt van de manier waarop Java's extends-sleutelwoord de functie-implementatie op passende wijze uitbreidt en overschrijft. Pas wanneer je Go's compositorische codehergebruik goed begrijpt, raak je het niet meer door elkaar halen met overerving, maar het is vanaf het begin moeilijk om dit theoretisch volledig te begrijpen. Laten we het bekijken aan de hand van scenario's waarin fouten snel worden gemaakt.

## Veelgemaakte fouten

Beginners kunnen de volgende fouten maken.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type Apple struct {
	Label string
	Name  string
	Brix  float64
}

type Watermelon struct {
	Label string
	Name  string
	Brix  float64
}

func (a *Apple) PrintAll() {
	fmt.Printf("Fruit: %s, Label: %s, Brix: %v\n", a.Name, a.Label, a.Brix)
}

const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
}

func main() {
	fmt.Println("Inheritance test #1")
	apple := new(Apple)
	watermelon := apple
	apple.SetLabel("Apple_1")
	fmt.Println("Apple, before copied to Watermelon")
	apple.PrintAll()
	watermelon.SetLabel("WaterMelon_2")
	fmt.Println("Apple, after copied to Watermelon")
	apple.PrintAll()
	fmt.Println("Watermelon, which inherited Apple's Method")
	watermelon.PrintAll()
}
```

Wanneer je denkt dat Go traditionele overerving volgt, lijkt deze code geen problemen te bevatten. De uitvoer ervan is echter als volgt.

```
Inheritance test #1
Succeed: Label was  Apple_1
Apple, before copied to Watermelon
Fruit: apple, Label: Apple_1, Brix: 14.5
Failed: Label was  WaterMelon_2
Apple, after copied to Watermelon
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
Watermelon, which inherited Apple's Method
Fruit: apple, Label: EMPTY LABEL, Brix: 14.5
```

Hier wordt het gedrag van Go pas echt duidelijk.

```go
watermelon := apple
```

Deze code zet Apple op geen enkele manier om in de Watermelon-klasse.
In feite is watermelon slechts een pointer naar apple.

Nogmaals, **Go volgt het traditionele concept van overerving niet.**

Wanneer je code schrijft met dit misverstand, ontstaan ernstige fouten zoals betekenisloze pointercreatie en het onbedoeld kopiëren van functies naar andere structs.

Hoe ziet modelcode er dan uit?

## Een geschikt voorbeeld in Go

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	Fruit BaseFruit
}

type Watermelon struct {
	Label string
	Fruit BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Fruit.Brix 	= 14.5;
	a.Fruit.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Fruit.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Fruit.Name, a.Label);
	a.Fruit.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Fruit.Brix = 10;
	w.Fruit.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Fruit.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Fruit.Name, w.Label);
	w.Fruit.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

Toch is het in Go mogelijk om overerving **te laten lijken**.
Dat is het voorbeeld van anonieme embedding. Dit is mogelijk door de geneste struct zonder naam te declareren.
In dit geval kun je **de velden van de onderliggende struct zonder expliciete vermelding gebruiken** en er rechtstreeks toegang toe krijgen.
Door het patroon te gebruiken waarbij de velden van de onderliggende struct naar de bovenliggende struct worden **gepromoveerd**, kan de leesbaarheid in sommige gevallen verbeteren. Wanneer de onderliggende struct expliciet zichtbaar moet zijn, raden we echter aan dit niet te gebruiken.

```go
package main
import (
	"fmt"
	"strings"
)

type Fruits interface {
	GetBrix() float64
	GetName() string
	SetLabel()
	GetLabel(string) string
	PrintAll()
}

type BaseFruit struct {
	Name  string
	Brix  float64
}

type Apple struct {
	Label string
	BaseFruit
}

type Watermelon struct {
	Label string
	BaseFruit

}

func (b *BaseFruit) PrintAll() {
	fmt.Printf("Fruit: %s, Brix: %v\n", b.Name, b.Brix)
}


const (
	NO_LABEL = "EMPTY LABEL"
)

func (a *Apple) SetLabel(lbl string) {
	a.Brix 	= 14.5;
	a.Name 	= "apple";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, a.Name) {
		fmt.Println("Succeed: Label was ", lbl)
		a.Label = lbl;
	} else {
		fmt.Println("Failed: Label was ", lbl)
		a.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", a.Name, a.Label);
	a.PrintAll()
}

func (w *Watermelon) SetLabel(lbl string) {
	w.Brix = 10;
	w.Name = "Watermelon";
	lbl_lower := strings.ToLower(lbl)
	if strings.Contains(lbl_lower, w.Name) {
		w.Label = lbl;
	} else {
		w.Label = NO_LABEL;
	}
	fmt.Printf("Fruit %s label set to %s\n", w.Name, w.Label);
	w.PrintAll()
}

func main() {
	apple := new(Apple)
	watermelon := new(Watermelon)
	apple.SetLabel("Apple_1")
	watermelon.SetLabel("WaterMelon_2")
}
```

In dit voorbeeld is er het volgende verschil.

```go
w.PrintAll() // Geen w.Fruit.PrintAll(), maar automatische promotie-aanroep via de anonieme struct
```

Beide voorbeelden hebben de volgende belangrijke punten:

- Houd main eenvoudig en verdeel functies per functionaliteit
- Gebruik voor verschillende structs verschillende objecten
- Gebruik een interne struct wanneer iets gedeeld moet worden

Welke voordelen heeft deze programmeerfilosofie?

## Voordelen

- Duidelijk onderscheid tussen methoden die gedeeld moeten worden en methoden die dat niet hoeven
- Scheiding van verantwoordelijkheden per afzonderlijke struct en methode
- Code die structureel is gescheiden volgens de vereiste functionele specificaties

In het begin is Go misschien ongewoon omdat het verschilt van traditionele OOP, maar naarmate je er vertrouwder mee wordt, wordt expliciet programmeren mogelijk.

## Samenvatting

- Isoleer verantwoordelijkheden
- Verdeel in detail per struct
- Methoden mogen niet worden begrepen als abstracte klassen zoals in Java
- Programmeer expliciet en concreet
  Go is eenvoudiger en duidelijker dan het traditionele OOP-model en moet afzonderlijk worden behandeld. Schrijf in plaats van uitbreidbaar te programmeren liever gefaseerd en gestructureerd door de code op te splitsen.
