Skip to content

Go Project Anatomy

Coming from Node.js/TypeScript, you are used to package.json, tsconfig.json, a src/ directory, and node_modules. Go projects have a different but equally structured layout. Understanding it up front will save you hours of confusion.

go.mod is created by go mod init and is the root manifest for your Go module. It declares the module name (your import path prefix) and the minimum Go version required.

TypeScript
// package.json
{
"name": "my-app",
"version": "1.0.0",
"scripts": { "build": "tsc", "start": "node dist/index.js" },
"dependencies": { "express": "^4.18.2" },
"devDependencies": { "typescript": "^5.0.0" }
}
Go
// go.mod
module github.com/yourname/myapp
go 1.22
require (
github.com/gin-gonic/gin v1.9.1
)
// go.sum is the lockfile (like package-lock.json)
// it is auto-managed — never edit it manually

The module name (github.com/yourname/myapp) is not just a name — it is the import path prefix used throughout your code. Conventionally it matches your source repository URL.

In Go, a package is a directory of .go files that all declare the same package name. This is stricter than TypeScript/ESM, where every file is its own module. In Go you import at the package level, not the file level.

TypeScript
// TypeScript: file-level imports
import { UserService } from './services/user.service';
import type { User } from './models/user.model';
import * as utils from '../shared/utils';
Go
// Go: package-level imports
import (
"github.com/yourname/myapp/internal/services"
"github.com/yourname/myapp/internal/models"
"github.com/yourname/myapp/pkg/utils"
)
// then use: services.NewUserService(...)
// models.User{...}
// utils.SomeHelper(...)

The Go community has converged on a standard layout for services and CLIs. Here it is compared to a typical Node/TypeScript service:

TypeScript
// TypeScript / Node.js project
my-app/
src/
controllers/ # route handlers
services/ # business logic
models/ # TypeScript interfaces/types
middleware/ # Express middleware
index.ts # entry point
dist/ # compiled output
package.json
tsconfig.json
.env
Go
// Go project (idiomatic layout)
myapp/
cmd/
myapp/
main.go # entry point (package main)
internal/
handlers/ # HTTP handlers
services/ # business logic
models/ # struct definitions
middleware/ # HTTP middleware
pkg/ # reusable packages (public API)
go.mod
go.sum
.env

Key rules:

  • cmd/ holds your entry points (main packages). One subdirectory per binary.
  • internal/ is Go-enforced private code — packages here cannot be imported by external modules.
  • pkg/ holds packages you intentionally expose for external use.

In TypeScript you use export and import. In Go, visibility is determined by the first letter of an identifier’s name — no export keyword needed.

TypeScript
// TypeScript: explicit export
export interface User { id: string; name: string; }
export function greet(name: string): string {
return `Hello, ${name}!`;
}
// internal helper (no export)
function formatName(n: string): string { return n.trim(); }
Go
// Go: uppercase = exported, lowercase = unexported
type User struct { // exported (capital U)
ID string
Name string
}
func Greet(name string) string { // exported (capital G)
return "Hello, " + formatName(name) + "!"
}
func formatName(n string) string { // unexported (lowercase)
return strings.TrimSpace(n)
}
package main
import (
"fmt"
"strings"
)
// Exported function — capital letter
func Greet(name string) string {
return "Hello, " + formatName(name) + "!"
}
// unexported helper — lowercase letter
func formatName(n string) string {
return strings.ToUpper(n[:1]) + n[1:]
}
func main() {
fmt.Println(Greet("gopher"))
fmt.Println(Greet("world"))
// The standard library is imported by path
// "strings" -> the strings package
fmt.Println(strings.ToUpper("go is great"))
}
In Go, what determines whether an identifier is exported (public)?
What does the `internal/` directory enforce in Go?
In Go, what is the scope of an import? (e.g. import "fmt")
What is go.sum equivalent to in the Node.js ecosystem?