Das Keyword type gibt einem Typ einen Namen. Es gibt es in zwei Formen. type UserID int64 ist eine Typdefinition. Sie erzeugt einen völlig neuen Typ mit eigener Identität und eigenen Methoden. type Handler = http.Handler ist eine Alias-Deklaration. Sie fügt einem bereits existierenden Typ einen zweiten Namen hinzu. Welche Form du wählst, entscheidet darüber, ob der Compiler dich die beiden Namen mischen lässt und ob du Methoden hinzufügen kannst (Go spec).
Kurzfassung
type Name Underlyingdefiniert einen neuen Typ.type Name = Otherdeklariert einen Alias. Ein Blocktype ( ... )fasst mehrere Deklarationen zusammen.- Ein definierter Typ unterscheidet sich von jedem anderen Typ, auch von einem mit demselben zugrunde liegenden Typ. Eine
UserIDlässt sich ohne explizite Konvertierung nicht dort übergeben, wo eineOrderIDoder einint64erwartet wird. - Untypisierte Konstanten und unbenannte Werte wie Map-Literale lassen sich einem definierten Typ trotzdem ohne Konvertierung zuweisen, also kompiliert
cancelOrder(42). - Ein definierter Typ beginnt ohne Methoden.
type Timestamp time.TimeverliertFormat. Bettetime.Timein ein Struct ein, wenn du die Methoden behalten willst. - Methoden kannst du nur für Typen aus deinem eigenen Package deklarieren.
func (d time.Duration) ...scheitert mitcannot define new methods on non-local type. - Ein Alias ist derselbe Typ unter einem anderen Namen.
byte,runeundanysind Aliase. Seit Go 1.24 können Aliase Typparameter haben. - Funktionstypen wie
http.HandlerFunckönnen Methoden haben. So macht Go aus einer gewöhnlichen Funktion einen Interface-Wert. - Generische Typen haben Typparameter, etwa
Page[T any]. Seit Go 1.27 können auch Methoden eigene Typparameter deklarieren.
Wie deklariert man einen Typ in Go?
Schreib type, einen Namen und den Typ, auf dem er basiert. Mehrere Deklarationen können sich einen Block type ( ... ) teilen, genau wie bei const- und var-Blöcken:
example.gogopackage main import ( "fmt" "net/http" "time" ) type ( UserID int64 OrderID int64 Order struct { ID OrderID UserID UserID CreatedAt time.Time } OrderStore interface { Get(id OrderID) (Order, error) } Handler = http.Handler ) func main() { o := Order{ID: 1001, UserID: 42} fmt.Println(o.ID) }
Das gibt 1001 aus. Der Block enthält vier Typdefinitionen und einen Alias. struct und interface sind die häufigsten rechten Seiten, und beide haben ein eigenes Keyword, das in Go Keywords: Alle 25 erklärt behandelt wird. Structs wie Order haben einen eigenen Leitfaden: Das Go-Keyword struct. Interfaces wie OrderStore haben einen eigenen Leitfaden: Das Go-Keyword interface.
Für Typnamen gelten die üblichen Sichtbarkeitsregeln. Order wird exportiert, weil es mit einem Großbuchstaben beginnt, und orderRow wäre nur innerhalb seines Packages sichtbar. Das Go-Keyword package erklärt exportierte Namen genauer.
Du kannst einen Typ auch innerhalb einer Funktion deklarieren. Er ist dann nur in dieser Funktion sichtbar, und das passt gut zu einmaligen Strukturen wie einem Request-Body:
example.gogofunc main() { type loginRequest struct { Email string `json:"email"` Password string `json:"password"` } var req loginRequest err := json.Unmarshal([]byte(`{"email":"[email protected]","password":"hunter2"}`), &req) fmt.Println(req.Email, err) }
Das gibt [email protected] <nil> aus. Ein lokaler Typ kann keine Methoden haben, weil Methodendeklarationen nur auf Package-Ebene existieren.
Was ist ein definierter Typ in Go?
Ein Typ, der als type Name Underlying deklariert ist, also ohne =. Die Spezifikation nennt das eine Typdefinition. Jeder definierte Typ hat einen zugrunde liegenden Typ, der bestimmt, wie seine Werte gespeichert werden und welche Operatoren auf ihnen funktionieren. UserID und OrderID oben haben beide den zugrunde liegenden Typ int64. Du kannst sie also addieren, vergleichen und ausgeben wie jeden int64.
Was sie nicht teilen, ist ihre Identität. Ein definierter Typ unterscheidet sich immer von jedem anderen Typ, deshalb weigert sich der Compiler, sie zu mischen:
example.gogopackage main import "fmt" type UserID int64 type OrderID int64 func cancelOrder(id OrderID) { fmt.Println("cancelling order", id) } func main() { var user UserID = 42 cancelOrder(user) }
example.texttext./main.go:14:14: cannot use user (variable of int64 type UserID) as OrderID value in argument to cancelOrder
Wegen dieses Fehlers lohnt es sich, ID-Typen zu definieren. Eine Funktion mit der Signatur refund(userID, orderID int64) akzeptiert ihre Argumente in beliebiger Reihenfolge. Mit UserID und OrderID ist ein Vertauschen ein Compilerfehler und keine Rückerstattung an den falschen Kunden.
Dieselbe Regel trennt einen definierten Typ von seinem zugrunde liegenden Typ. Auch eine UserID an eine Funktion zu übergeben, die einen int64 erwartet, scheitert:
example.texttext./main.go:13:11: cannot use id (variable of int64 type UserID) as int64 value in argument to loadUser
Wie konvertiert man zwischen definierten Typen?
Schreib den Zieltyp wie einen Funktionsaufruf. OrderID(user) und int64(user) kompilieren beide, weil eine Konvertierung immer dann erlaubt ist, wenn zwei Typen denselben zugrunde liegenden Typ haben. Die Konvertierung kostet zur Laufzeit nichts. Der Wert bleibt gleich, und nur der Typ, den der Compiler sieht, ändert sich.
Konvertierungen sind absichtlich explizit. Wenn du in einem Code Review OrderID(user) siehst, weißt du, dass jemand entschieden hat, eine User-ID als Bestell-ID zu behandeln, und du kannst nach dem Grund fragen.
Warum kannst du 42 oder ein Map-Literal ohne Konvertierung übergeben?
Keiner der beiden hat bisher einen definierten Typ. Die Zuweisbarkeit hat ein paar Regeln über „die Typen sind identisch“ hinaus, und zwei davon begegnen dir ständig (Go spec).
Die erste betrifft untypisierte Konstanten. 42 ist eine untypisierte Konstante und kann zu jedem Typ werden, dessen zugrunde liegender Typ sie darstellen kann. Deshalb kompiliert cancelOrder(42) und gibt cancelling order 42 aus. Es ist dieselbe Regel, mit der du 5 * time.Second schreiben kannst, denn time.Duration ist selbst ein definierter Typ mit dem zugrunde liegenden Typ int64. Das Go-Keyword const erklärt untypisierte Konstanten ausführlich.
Die zweite betrifft unbenannte Typen. Ein Wert kann einem definierten Typ zugewiesen werden, wenn beide identische zugrunde liegende Typen haben und mindestens einer der beiden kein benannter Typ ist. map[string]string ist ein Typliteral ohne Namen, also akzeptiert eine Funktion, die Headers erwartet, es direkt:
example.gogotype Headers map[string]string func send(h Headers) { fmt.Println(len(h), h["Content-Type"]) } func main() { send(map[string]string{"Content-Type": "application/json"}) raw := map[string]string{"Accept": "text/plain"} send(raw) }
Beide Aufrufe kompilieren. Die Regel funktioniert genauso für []byte, func(...) und Struct-Literale. Sie erlaubt aber nie den direkten Weg von einem benannten Typ zu einem anderen, etwa von UserID zu OrderID oder von UserID zu int64.
Warum erbt ein definierter Typ keine Methoden?
Ein definierter Typ übernimmt die Struktur des zugrunde liegenden Typs, aber keine seiner Methoden. Das überrascht viele, wenn sie zum ersten Mal time.Time einwickeln:
example.gogopackage main import ( "fmt" "time" ) type Timestamp time.Time func main() { created := Timestamp(time.Now()) fmt.Println(created.Format(time.RFC3339)) }
example.texttext./main.go:12:22: created.Format undefined (type Timestamp has no field or method Format)
Die Methodenmenge beginnt leer, damit sich der neue Typ anders verhalten kann als der alte. http.Header ist eine map[string][]string mit Methoden wie Get und Set, die Header-Namen kanonisieren, und sort.StringSlice ist ein []string mit den Methoden, die sort.Interface braucht. Würden definierte Typen jede Methode ihres zugrunde liegenden Typs kopieren, bekäme Timestamp Methoden, die du ihm nie geben wolltest.
Wenn du die alten Methoden zurückhaben willst, hast du zwei Möglichkeiten. Die erste ist, dort in den ursprünglichen Typ zu konvertieren, wo du sie brauchst. Eine so geschriebene Methode String bestimmt, wie der Wert ausgegeben wird:
example.gogotype Timestamp time.Time func (t Timestamp) String() string { return time.Time(t).Format(time.RFC3339) }
Die zweite ist Embedding. Ein Struct, das time.Time einbettet, übernimmt alle seine Methoden, und du fügst deine eigenen daneben hinzu:
example.gogopackage main import ( "fmt" "time" ) type Timestamp struct { time.Time } func (t Timestamp) ISO() string { return t.UTC().Format(time.RFC3339) } func main() { created := Timestamp{time.Date(2026, 9, 25, 14, 30, 0, 0, time.UTC)} fmt.Println(created.Format("2006-01-02")) fmt.Println(created.ISO()) }
Das gibt 2026-09-25 und danach 2026-09-25T14:30:00Z aus. Mit Embedding bleiben Format, Before, Unix und alle anderen Methoden von time.Time erhalten. Außerdem erfüllt Timestamp damit jedes Interface, das time.Time erfüllt, auch json.Marshaler. Es wird also als RFC-3339-String nach JSON kodiert und nicht als Objekt.
Kann man einem eingebauten oder importierten Typ Methoden hinzufügen?
Nein. Eine Methode muss im selben Package deklariert werden wie der Typ ihres Receivers. Eine Methode für time.Duration hinzuzufügen scheitert:
example.gogofunc (d time.Duration) Minutes10() float64 { return d.Minutes() / 10 }
example.texttext./main.go:5:9: cannot define new methods on non-local type time.Duration
Dasselbe passiert mit int, string und jedem anderen vordeklarierten Typ sowie mit einem Alias für einen Typ aus einem anderen Package. Die Regel verhindert, dass zwei Packages demselben Typ widersprüchliche Methoden anhängen. Wenn du zusätzliches Verhalten brauchst, definier deinen eigenen Typ, etwa type Cents int64, und leg die Methoden dort an.
Was ist ein Typalias in Go?
Ein Alias ist ein zweiter Name für einen Typ, der bereits existiert. Man schreibt ihn mit =:
example.gogotype Handler = http.Handler
Handler und http.Handler sind identische Typen. Du kannst den einen übergeben, wo der andere erwartet wird, der Compiler verlangt nie eine Konvertierung, und %T gibt den ursprünglichen Namen aus. Ein Alias erzeugt auch keine neue Methodenmenge. Eine Methode für einen Alias zu deklarieren ist dasselbe, wie sie für den ursprünglichen Typ zu deklarieren, also gilt die Regel zu „non-local“ Typen auch hier.
Go verwendet Aliase für seine eigenen vordeklarierten Namen. byte ist ein Alias für uint8, rune für int32 und any für interface{}. Deshalb lässt sich ein []byte ohne Konvertierung an eine Funktion übergeben, die einen []uint8 erwartet.
Aliase kamen mit Go 1.9 hinzu, vor allem damit ein Typ zwischen Packages umziehen kann, ohne Aufrufer kaputtzumachen. Wenn eine große Codebasis Order von billing nach orders verschiebt, behält das alte Package eine Zeit lang type Order = orders.Order. Code, der noch billing importiert, kompiliert weiter, und die Aufrufer ziehen einer nach dem anderen um (Go blog).
Kann ein Typalias generisch sein?
Ja, seit Go 1.24. Ein Alias kann Typparameter deklarieren und muss dort, wo er verwendet wird, instanziiert werden (Go 1.24 Release Notes):
example.gogotype Set[K comparable] = map[K]struct{} func main() { allowed := Set[string]{"GET": {}, "HEAD": {}} var plain map[string]struct{} = allowed _, ok := plain["GET"] fmt.Println(ok, len(allowed)) fmt.Printf("%T\n", allowed) }
Das gibt true 2 und danach map[string]struct {} aus. Set[string] ist weiterhin nur ein Name für map[string]struct{}, deshalb braucht die Zuweisung an plain keine Konvertierung.
Definierter Typ vs. Typalias: Was ist der Unterschied?
Definierter Typ (type UserID int64) | Alias (type Handler = http.Handler) | |
|---|---|---|
| Neuer Typ? | Ja, unterscheidet sich von jedem anderen Typ | Nein, identisch mit dem Original |
| Mischen mit dem Original | Braucht eine explizite Konvertierung | Funktioniert direkt |
| Methoden des Originals | Werden nicht geerbt | Dieselben Methoden, da es derselbe Typ ist |
| Kann neue Methoden deklarieren | Ja, im selben Package | Nur wenn das Original ein definierter Typ in deinem Package ist |
%T gibt aus | main.UserID | Den ursprünglichen Namen |
| Typische Verwendung | IDs, Einheiten, Enums, Domänentypen | Typen zwischen Packages verschieben, kürzere Namen |
Meistens willst du einen definierten Typ. Greif zu einem Alias, wenn zwei Namen exakt denselben Typ bezeichnen müssen, meist während eines Refactorings.
Wie funktionieren Funktionstypen in Go?
Eine Funktionssignatur ist ein Typ wie jeder andere, du kannst ihr also mit type einen Namen geben. Sobald sie einen Namen hat, kann sie Methoden haben. Die Standardbibliothek nutzt das, um aus einer gewöhnlichen Funktion einen Interface-Wert zu machen. http.Handler ist ein Interface mit einer einzigen Methode, ServeHTTP, und eine Funktion ist für sich allein kein Handler:
example.gogofunc health(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "ok") } var h http.Handler = health
example.texttext./main.go:13:23: cannot use health (value of type func(w http.ResponseWriter, r *http.Request)) as http.Handler value in variable declaration: func(w http.ResponseWriter, r *http.Request) does not implement http.Handler (missing method ServeHTTP)
net/http deklariert type HandlerFunc func(ResponseWriter, *Request) und gibt ihm eine Methode ServeHTTP, die die Funktion selbst aufruft. Die Konvertierung mit http.HandlerFunc(health) ergibt einen Wert, der das Interface erfüllt. http.HandleFunc und mux.HandleFunc erledigen diese Konvertierung für dich (net/http docs). Das Go-Keyword func erklärt Funktionswerte, Closures und Method Values im Detail.
Dasselbe Muster funktioniert für deine eigenen Policies. Eine Retry-Policy ist eine Funktion von der Versuchsnummer zu einer Wartezeit, und eine Methode auf dem Funktionstyp kann sie umhüllen:
example.gogopackage main import ( "fmt" "time" ) type RetryPolicy func(attempt int) time.Duration func (p RetryPolicy) Capped(limit time.Duration) RetryPolicy { return func(attempt int) time.Duration { return min(p(attempt), limit) } } func Exponential(base time.Duration) RetryPolicy { return func(attempt int) time.Duration { return base << attempt } } func main() { policy := Exponential(100 * time.Millisecond).Capped(time.Second) for attempt := range 5 { fmt.Println(attempt, policy(attempt)) } }
example.texttext0 100ms 1 200ms 2 400ms 3 800ms 4 1s
Der Name sagt Lesern, wofür die Funktion da ist, und der Aufruf von Capped sieht aus wie ein Methodenaufruf auf jedem anderen Wert. Ein HTTP-Client kann ein Feld vom Typ RetryPolicy haben, und Tests können eine Policy übergeben, die immer 0 zurückgibt.
Wie funktionieren generische Typen in Go?
Eine Typdeklaration kann nach dem Namen Typparameter in eckigen Klammern haben. Eine paginierte API-Antwort ist ein typischer Fall, weil die Paging-Felder für jede Art von Element gleich sind:
example.gogopackage main import ( "fmt" "strconv" ) type Page[T any] struct { Items []T NextCursor string } func (p Page[T]) HasMore() bool { return p.NextCursor != "" } func (p Page[T]) Map[U any](fn func(T) U) Page[U] { out := Page[U]{NextCursor: p.NextCursor} for _, item := range p.Items { out.Items = append(out.Items, fn(item)) } return out } type Order struct { ID int64 Total int64 } func main() { orders := Page[Order]{ Items: []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}}, NextCursor: "b3JkZXI6Mg", } ids := orders.Map(func(o Order) string { return strconv.FormatInt(o.ID, 10) }) fmt.Println(ids.Items, ids.HasMore()) }
Das gibt [1 2] true aus. Page[Order] und Page[string] sind zwei verschiedene Typen, die aus einer einzigen Deklaration entstehen. Methoden wiederholen den Typparameter im Receiver, wie in func (p Page[T]).
Map deklariert seinen eigenen Typparameter U. Das ist eine generische Methode, und die gibt es erst seit Go 1.27 (Go 1.27 Release Notes). Mit go 1.26 oder älter in der go.mod scheitert derselbe Code mit:
example.texttext./main.go:17:22: generic method requires go1.27 or later (-lang was set to go1.26; check go.mod)
Interface-Methoden können weiterhin keine Typparameter haben. Map[U any](fn func(string) U) []U in einem Interface scheitert mit interface method must have no type parameters. Eine generische Methode zählt also nie dazu, wenn es darum geht, ein Interface zu erfüllen.
Wo kommt type in Go noch vor?
Was macht .(type) in einem Type Switch?
x.(type) ist nur in einem switch gültig und verzweigt nach dem dynamischen Typ eines Interface-Werts. Ein häufiger Einsatz sind Fehler:
example.gogofunc describe(err error) string { switch e := err.(type) { case nil: return "ok" case *fs.PathError: return "file error on " + e.Path case net.Error: return fmt.Sprintf("network error, timeout=%v", e.Timeout()) default: return "other: " + e.Error() } }
In jedem Fall hat e den Typ dieses Falls, also kompilieren e.Path und e.Timeout() ohne separate Type Assertion. Rufst du describe mit dem Fehler von os.Open("/etc/levelupgo.yaml") auf, gibt es file error on /etc/levelupgo.yaml aus, wenn die Datei nicht existiert.
Ein Type Switch betrachtet nur den äußersten Fehler. Nach fmt.Errorf("load config: %w", err) landet derselbe Aufruf im default. Für gewrappte Fehler verwendest du errors.As, das die Kette durchläuft:
example.gogovar pathErr *fs.PathError if errors.As(wrapped, &pathErr) { fmt.Println("errors.As found", pathErr.Path) }
Wie wird type in generischen Constraints verwendet?
Ein Constraint ist ein Interface, meist mit type deklariert, und kann sowohl Typen als auch Methoden auflisten. Die ~ vor einem Typ bedeutet „jeder Typ, dessen zugrunde liegender Typ dieser ist“, und genau das lässt definierte Typen durch:
example.gogotype Cents int64 type Amount interface { ~int64 | ~float64 } func Sum[T Amount](values []T) T { var total T for _, v := range values { total += v } return total }
Sum([]Cents{4999, 1250}) gibt 6249 als Cents zurück. Ohne die ~ wird Cents abgelehnt, und der Compiler zeigt auf die Lösung:
example.texttext./main.go:20:17: Cents does not satisfy Amount (possibly missing ~ for int64 in Amount)
Auch Enums bauen in Go auf definierten Typen auf. Ein Typ wie type OrderStatus int zusammen mit einem const-Block mit iota ergibt eine typisierte Liste von Werten. Das Go-Keyword const geht das Schritt für Schritt durch, und das Go-Keyword var erklärt, wie Variablen dieser Typen mit ihrem Nullwert beginnen.
Wo LevelUpGo ins Spiel kommt
LevelUpGo bringt dir Go mit Übungen bei, die echten Go-Code im Browser ausführen. Composite Types baut Structs, Konstruktorfunktionen und Methoden Schritt für Schritt auf. Interfaces & Polymorphism behandelt implizite Interfaces, Type Assertions und Type Switches. Go Generics Masterclass geht generische Typen und eigene Constraints durch. Der Training Ground bietet kurze, eigenständige Übungen zum Üben außerhalb eines Kurses. Die anderen 24 reservierten Wörter findest du in Go Keywords: Alle 25 erklärt.
Häufig gestellte Fragen
Ist type ein Keyword in Go?
Ja. type ist eines der 25 reservierten Keywords von Go, du kannst es also nicht als Variablen- oder Funktionsnamen verwenden. Es leitet eine Typdeklaration ein, entweder eine Typdefinition wie type UserID int64 oder einen Alias wie type Handler = http.Handler. Außerdem kommt es in Type Switches als x.(type) vor.
Was ist der Unterschied zwischen einer Typdefinition und einem Typalias in Go?
Eine Typdefinition, type UserID int64, erzeugt einen neuen Typ, der sich von int64 unterscheidet und ohne Methoden beginnt. Ein Alias, type ID = int64, ist ein weiterer Name für int64 selbst. Werte eines definierten Typs brauchen eine Konvertierung, um sie mit dem ursprünglichen Typ zu mischen. Ein Alias braucht keine, weil beide Namen denselben Typ bezeichnen.
Kann man in Go einem eingebauten Typ Methoden hinzufügen?
Nein. Methoden lassen sich nur für Typen deklarieren, die im selben Package definiert sind. func (n int) IsEven() bool scheitert deshalb mit cannot define new methods on non-local type int. Definier deinen eigenen Typ, etwa type Quantity int, und deklarier die Methode darauf.
Warum meldet Go „cannot use id (variable of int64 type UserID) as int64 value“?
Weil UserID ein definierter Typ ist und Go einen typisierten Wert nie von selbst von einem benannten Typ in einen anderen konvertiert, auch wenn der zugrunde liegende Typ derselbe ist. Nur untypisierte Konstanten wie 42 passen sich dem benötigten Typ an. Konvertier explizit mit int64(id). Wenn du ständig konvertierst, sollte die Funktion wahrscheinlich stattdessen eine UserID erwarten.
Was macht .(type) in Go?
Es ist die Type-Switch-Form einer Type Assertion. switch e := err.(type) prüft den dynamischen Typ eines Interface-Werts gegen jeden case, und innerhalb eines Falls hat e den Typ dieses Falls. Das funktioniert nur in einem switch. Um auf einen einzelnen Typ zu prüfen, verwendest du v, ok := x.(T), und für gewrappte Fehler errors.As.
Quellen
- The Go Programming Language Specification, Type declarations: https://go.dev/ref/spec#Type_declarations
- The Go Programming Language Specification, Alias declarations: https://go.dev/ref/spec#Alias_declarations
- The Go Programming Language Specification, Assignability: https://go.dev/ref/spec#Assignability
- The Go Programming Language Specification, Method declarations: https://go.dev/ref/spec#Method_declarations
- The Go Programming Language Specification, Type switches: https://go.dev/ref/spec#Type_switches
- Go 1.24 Release Notes, generische Typaliase: https://go.dev/doc/go1.24
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
- The Go Blog, What's in an (Alias) Name?: https://go.dev/blog/alias-names
- net/http, HandlerFunc: https://pkg.go.dev/net/http#HandlerFunc
