Skip to content
ExponaBLOG
GO PROGRAMMING & PRODUCTION APIS · DAY 003 / 100

Go syntax essentials: a practical guide for developers

The basics in one post.

Expona Admin7 min read

Go syntax essentials: a practical guide for developers

If you're moving to Go from languages like Python, JavaScript, or Java, you'll notice the syntax is deliberately minimal and opinionated. We've found that once you grasp a handful of core constructs—variable declaration, multiple return values, loop structure, and naming conventions—you're ready to build production APIs. This guide covers exactly what you need to know to read and write idiomatic Go code confidently.

var and := for variables

Go gives you two primary ways to declare variables, and understanding when to use each is your first step toward clean code.

The var keyword is the explicit declaration. It's verbose but clear, and it's what you use when you need to declare a variable without immediately assigning a value, or when you want to be explicit about type:

go
var count int
var name string = "Alice"
var ratio float64 = 3.14
var isActive bool

The := operator is the short declaration syntax. Inside functions, it's the more idiomatic choice when you're assigning an initial value. Go infers the type from the right-hand side:

go
count := 42                    // inferred as int
name := "Bob"                  // inferred as string
ratio := 2.71                  // inferred as float64
isActive := true               // inferred as bool

In production API code, you'll use := far more often. It's faster to write and just as readable. However, var has its place: at package scope (outside functions), you must use var because := only works inside functions. You'll also use var when declaring a variable that you'll assign to later:

go
var err error
if someCondition {
    err = doSomething()
}

A practical tip from our experience: keep scope tight. Declare variables as close as possible to where you use them. This makes the code easier to follow and reduces mental load when reading handler functions or service methods.

Functions return multiple values

One of Go's most powerful features is the ability to return multiple values from a single function. This is how Go handles error cases idiomatically, and it eliminates the need for exceptions or special sentinel values.

Consider a division function. In many languages, you'd throw an exception or return a special value on error. In Go, you return both the result and an error:

go
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}

When you call this function, you handle both return values:

go
result, err := divide(10, 2)
if err != nil {
    log.Fatal(err)
}
fmt.Println(result)  // 5

If you don't care about one of the return values, use the blank identifier _:

go
result, _ := divide(10, 2)
fmt.Println(result)

We strongly discourage this pattern in production code. Always check errors explicitly. Ignoring an error (even with _) usually masks bugs. In API handlers, database operations, and service methods, every error path matters.

Multiple return values also work beyond error handling. A function that parses a request might return both parsed data and a validation error:

go
func parseUserRequest(body []byte) (User, error) {
    var u User
    err := json.Unmarshal(body, &u)
    if err != nil {
        return User{}, err
    }
    return u, nil
}

Or a function that queries a database might return the record, a boolean indicating existence, and an error:

go
func getUserByID(ctx context.Context, id string) (User, bool, error) {
    // logic here
}

Keep the number of return values reasonable. Two or three is typical and readable. If a function returns more than four values, consider grouping related returns into a struct.

for is the only loop

Go has no while, no do...while, and no foreach. There's only for, and it's flexible enough to cover every looping scenario.

The classic for loop with initialization, condition, and increment:

go
for i := 0; i < 10; i++ {
    fmt.Println(i)
}

A while-style loop (just the condition):

go
for count < 100 {
    count += 10
}

An infinite loop (useful with break or return):

go
for {
    msg := <-channel
    if msg == "done" {
        break
    }
    process(msg)
}

Iterating over slices and arrays with range:

go
items := []string{"apple", "banana", "cherry"}
for i, item := range items {
    fmt.Printf("%d: %s\n", i, item)
}

Iterating over maps:

go
config := map[string]string{
    "host": "localhost",
    "port": "8080",
}
for key, value := range config {
    fmt.Printf("%s=%s\n", key, value)
}

If you only need the value (not the index or key), use _:

go
for _, item := range items {
    process(item)
}

In production APIs, you'll often iterate over database results, request bodies, and configuration files. The range keyword handles all of these cleanly. A real example from request processing:

go
func validateHeaders(headers map[string][]string) error {
    required := []string{"Authorization", "Content-Type"}
    for _, header := range required {
        if _, ok := headers[header]; !ok {
            return fmt.Errorf("missing header: %s", header)
        }
    }
    return nil
}

Exported names start with a capital letter

Go has no public, private, or protected keywords. Instead, visibility is determined by the first letter of the name. This rule applies to variables, functions, types, and struct fields.

Uppercase first letter means exported (visible outside the package):

go
type User struct {
    ID    int       // exported
    Name  string    // exported
    Email string    // exported
}

func GetUser(id int) (User, error) {  // exported function
    // logic
}

Lowercase first letter means unexported (package-private):

go
type config struct {         // unexported
    databaseURL string        // unexported
}

func parseConfig() config {   // unexported helper
    // logic
}

This convention forces you to think about your API surface. What should users of your package depend on? What's internal implementation? In a typical API service, your domain types (User, Product, Order) are exported. Your internal helper functions and configuration types are not.

A real-world example from a user service package:

go
// user/service.go
package user

type Service struct {
    db *sql.DB         // unexported
}

type User struct {
    ID    string
    Email string
    Name  string
}

func NewService(db *sql.DB) *Service {  // exported constructor
    return &Service{db: db}
}

func (s *Service) GetByID(ctx context.Context, id string) (User, error) {
    // exported method, clients call this
}

func (s *Service) validateEmail(email string) error {
    // unexported helper, internal only
}

Clients of this package import it and use user.Service, user.User, and user.NewService. They never see or interact with the unexported types and methods.

Common mistakes

We've seen a few patterns that trip up developers new to Go:

  • Ignoring errors silently. Using _, _ := functionReturningError() silently hides problems. Always check errors explicitly, even if you just log them.
  • Mixing var and := inconsistently. Pick one style per code block for readability. Inside functions, prefer :=. At package scope, use var.
  • Assuming lowercase names are truly private. They're package-private, not file-private. Any code in the same package can access them. This is a feature, not a bug, but it's worth understanding.
  • Not handling the comma-ok idiom in maps. When accessing a map, always check if the key exists: value, ok := myMap[key]. Missing this check will assign the zero value silently.
  • Over-returning from functions. If a function returns five values, consider refactoring. Group related return values into a struct.

Key takeaways

  • Use := inside functions for brevity and idiom; use var at package scope and when declaring without initialization.
  • Leverage multiple return values for error handling. Always check the error return value explicitly.
  • The for loop covers every iteration scenario: classic loops, while-style conditions, infinite loops, and range iteration.
  • Exported names start with capital letters; unexported names start with lowercase. This simple rule defines your package's public API.
  • These four concepts form the foundation. Master them, and you're ready to build production Go services with confidence.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles