Struct Type Declarations

Learn how to declare struct types in Go, define fields with names and types, and use anonymous structs for one-off data grouping.

A struct type is a template you create to group related data fields into a single unit. You declare the type once, then use it to produce as many concrete values as you need. Each field has a name and a type, and the compiler enforces that every value you construct from that template respects those definitions.

This page covers only the act of declaring the type itself — writing the type ... struct { ... } block.

After This Section:

Once you finish reading, open a Go file, write type Person struct { Name string; Age int }, and compile. If your editor shows no errors and you can refer to Person as a type in the same package, you've successfully declared a named struct type.

Named Struct Type Syntax

A named struct type is the most common way to define a struct. You give the type a name so you can reuse it across functions, packages, and methods.

The declaration always follows this pattern:

type TypeName struct {
    FieldName1 FieldType1
    FieldName2 FieldType2
    // ...
}

type tells Go you are creating a new type. TypeName is any valid Go identifier (capitalized if you want it exported, lowercase if package‑private). The struct keyword marks the body as a collection of named fields. Every field inside the braces gets a name and a type. Fields can be any Go type — primitives, slices, maps, other structs, even function types — but they cannot be complex type expressions without parentheses unless you wrap them.

A concrete example:

package main
import "fmt"
type Employee struct {
    FirstName string
    LastName  string
    Age       int
    Salary    float64
}
func main() {
    var emp Employee
    fmt.Printf("%+v\n", emp)
}

When you run this, Go prints {FirstName: LastName: Age:0 Salary:0}. The variable emp exists because Employee is now a real type you can use. The fields are populated with their zero values because we declared the variable without initialising it. This reveals the type’s shape even before you put data into it.

If multiple consecutive fields share the same type, you can collapse them into a single line:

type Address struct {
    Street, City, State string
    ZipCode             int
}

This saves vertical space but makes the field list harder to scan when the shared type appears far to the right. Many teams forbid the collapsed form in production code for readability; it is a stylistic choice, not a performance one.

Readability Over Brevity:

Collapsing fields of the same type hides individual fields at a glance. In a struct with a dozen fields, a reader skimming the left edge sees only Street and stops — the fact that City and State are also present requires horizontal parsing. Use the compact form only for tiny structs where all fields are obviously related.

Exporting Struct Fields and Types

Go’s visibility rule is simple: an identifier starting with an uppercase letter is exported (accessible from other packages), while a lowercase start keeps it unexported (package‑private). This applies to both the struct type name and each field name.

// Exported type with a mix of exported and unexported fields.
type User struct {
    Name      string // exported
    email     string // unexported
    AccountID int    // exported
}

If you declare the type as type user struct { ... } (lowercase), outside packages cannot refer to user at all, even if they import the package.

Why Unexported Fields Exist:

Unexported fields let you store internal state that other packages should not touch directly. They are still accessible indirectly through exported methods, which gives you control over invariants — for example, a struct that maintains a cache might have an unexported data field only modified through a getter that populates it on first access.

The Type Keyword Creates a Distinct Type

When you write type Employee struct { ... }, you are creating a brand‑new named type, not an alias. Employee and struct { FirstName string; ... } have the same underlying type, but Go’s type system treats them as distinct. This matters for assignability, comparability, and method sets.

For example, you cannot assign an anonymous struct literal to a named struct variable directly unless you convert it:

type Point struct{ X, Y int }
var p Point
// p = struct{ X, Y int }{1, 2}  // compile error: cannot use ... as type Point
p = Point(struct{ X, Y int }{1, 2}) // explicit conversion works because underlying types match

Beginners often bump into this when they see two structs with identical field lists and wonder why one cannot be used where the other is expected. The answer is that Go’s type identity is nominal, not structural — the name matters.

Anonymous Struct Types

You can declare a struct type without giving it a name. This is done by writing struct { ... } directly where a type is expected, such as in a variable declaration, a function parameter, or a composite literal.

func main() {
    car := struct {
        Make  string
        Model string
        Year  int
    }{
        Make:  "Toyota",
        Model: "Corolla",
        Year:  2021,
    }
    fmt.Println(car.Make)
}

Here, the type exists only at this spot. You cannot create another variable of exactly the same type elsewhere in the package without repeating the full struct declaration — and even if you do, the two anonymous struct types are considered identical only if the full structural definition matches exactly (same field names, types, and order), but they are still unnamed. In practice, you rarely want to replicate an anonymous struct type across functions; that’s a signal you should give it a name.

When Anonymous Structs Make Sense

  • Table‑driven tests: You often need a quick record of inputs and expected outputs. An anonymous struct inside a slice literal keeps the test table self‑contained.
  • Decoding a nested JSON object that you only need once: You can define an anonymous struct right at the point of unmarshalling without polluting the package namespace.
  • Returning a one‑off result from a function when the caller will immediately destructure it: Though named structs are usually better for clarity, anonymous types are acceptable for internal helpers.
tests := []struct {
    name     string
    input    int
    expected int
}{
    {"zero", 0, 0},
    {"positive", 5, 25},
}

Don’t Try to Define a Named Struct Inside a Function:

A frequent beginner mistake is attempting type User struct { ... } inside a function body. That produces a compile error: type declaration is not allowed inside a function. Named types can only appear at the package level. Inside functions, you are limited to anonymous struct types. If you find yourself reaching for a name, promote the declaration to package scope.

How Struct Types Look to the Compiler

When the compiler sees a struct type declaration, it computes the memory layout of the fields. The fields are placed in memory in source order, subject to alignment and padding (explored in field alignment). The type becomes known by its name (if named) and its structural identity. A named struct type is a distinct type even if its structure matches another named type.

A struct type with no fields — struct{} — is a valid type that occupies zero bytes. It is used for signalling (e.g., channel of struct{} when you only care about the event, not the value) or as a set element when using maps for membership checks.

Common Mistakes and Misconceptions

Forgetting the struct Keyword:

Writing type Point { X, Y int } without the struct keyword will not compile. The compiler will tell you expected 'struct', found '{'. The struct token is mandatory.

Believing Order Doesn't Matter:

The order of fields in a struct declaration does matter. Two struct types with the same fields in different orders are different types. type A struct { a int; b bool } and type B struct { b bool; a int } have different underlying structures and are not interchangeable.

Assuming Uninitialized Fields Are nil:

Fields of a struct are not nil unless the field type is a pointer, slice, map, channel, or interface. A string field defaults to "", not nil. An int field defaults to 0. This is the zero‑value behavior.

Summary

You declare a struct type to give a name to a collection of related fields. The declaration creates a template that the compiler understands and enforces. Named struct types are the backbone of data modelling in Go; you will use them constantly. Anonymous struct types offer a lightweight alternative when the shape of the data is needed only once and naming it would clutter the package.

The declaration itself is just the first step. To actually use a struct type, you need to create values from it — and that is exactly what struct literals are for.