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.