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:
var count int
var name string = "Alice"
var ratio float64 = 3.14
var isActive boolThe := 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:
count := 42 // inferred as int
name := "Bob" // inferred as string
ratio := 2.71 // inferred as float64
isActive := true // inferred as boolIn 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:
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:
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:
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5If you don't care about one of the return values, use the blank identifier _:
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:
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:
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:
for i := 0; i < 10; i++ {
fmt.Println(i)
}A while-style loop (just the condition):
for count < 100 {
count += 10
}An infinite loop (useful with break or return):
for {
msg := <-channel
if msg == "done" {
break
}
process(msg)
}Iterating over slices and arrays with range:
items := []string{"apple", "banana", "cherry"}
for i, item := range items {
fmt.Printf("%d: %s\n", i, item)
}Iterating over maps:
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 _:
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:
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):
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):
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:
// 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, usevar. - 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; usevarat package scope and when declaring without initialization. - Leverage multiple return values for error handling. Always check the error return value explicitly.
- The
forloop 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.






Comments
Loading comments…