10.1 Defining Structs and Instantiating Them
Let’s get one thing straight: you’re not dealing with Java classes here. A Go struct is a beautifully simple, brutally efficient collection of named fields. It’s a way to say, “These pieces of data belong together,” without the ceremony of a full-blown object-oriented system.
You define one with the type and struct keywords. It looks like this:
type User struct {
ID int
Username string
Email string
IsActive bool
LastLogin time.Time
}
Congratulations, you’ve just created a new type, User. It’s now a first-class citizen in your program, just like int or string. You can use it in function signatures, as a slice element, or as a map value. This is the first big win: creating a vocabulary for your domain.
Instantiating Your New Type
There are a few ways to bring your User struct to life. The most explicit way is to use a struct literal, declaring each field. This is great for clarity but verbose.
// Method 1: Verbose and clear. Perfect for when you need to be explicit.
u1 := User{
ID: 142,
Username: "wizard_of_oz",
Email: "dot@kansas.gov",
IsActive: true,
LastLogin: time.Now(),
}
Now, what if you hate typing? Go’s got you covered. You can create a struct without naming the fields, but you must provide every value in the exact order the fields were declared. Miss one, and the compiler will throw a fit.
// Method 2: The "I live dangerously" compact form. Order is EVERYTHING.
u2 := User{143, "tin_man", "oil@me.com", false, time.Now()}
This is fantastic for small structs you use every day, like Point{x, y}. For anything with more than two or three fields, it’s a maintenance nightmare. Change the order of the struct fields later, and your code will silently assign values to the wrong fields. It’s a footgun. Use it sparingly.
The Power of the Zero Value
Here’s a classic Go idiom: the zero value. When you declare a variable of a struct type without initializing it, it’s not nil; it’s a fully-formed struct where every field is set to its own zero value.
var u3 User // All fields are zero: 0, "", "", false, time.Time{}
fmt.Println(u3.Username) // Outputs: "" (an empty string)
This is incredibly useful. The struct is always in a valid state. You never have to worry about null pointer exceptions because there are no pointers here (unless you put one in a field yourself). This design choice eliminates whole categories of bugs.
The Common new Function Misconception
You can also use the built-in new function. This doesn’t work like in other languages.
u4 := new(User) // This returns a pointer to a User, *User
fmt.Printf("%T\n", u4) // *main.User
// It's functionally identical to:
u5 := &User{} // This is the idiomatic way, by the way.
Both u4 and u5 are pointers to a User struct whose fields are all set to their zero values. The new keyword is rarely seen in the wild because the &User{} syntax is more expressive and common.
The Real World: Using Constructors (NewX)
While the zero value is often enough, sometimes you need validation or more complex setup logic. This is where you write a constructor function. By convention, it’s named NewX.
func NewUser(username, email string) (*User, error) {
// Validate input first. This is the whole point.
if username == "" {
return nil, fmt.Errorf("username cannot be empty")
}
if !strings.Contains(email, "@") {
return nil, fmt.Errorf("email must contain an '@'")
}
// Return a pointer to a properly initialized User.
return &User{
ID: generateID(), // some function you wrote
Username: username,
Email: email,
IsActive: true, // Default to active on creation
LastLogin: time.Now(),
}, nil
}
This pattern is gold. It gives you a controlled, documented way to create instances, ensures they start in a valid state, and allows for error handling right at the point of creation. It’s not magic, just a function. Its simplicity is its strength.