A Go project from one file is easy. But from ten?
\n\nImagine: you are writing a log parsing utility in Go. At first, everything fits into one file main.go. It works, everything is fine. But after a week, the code grows: functions for reading files, for filtering, for sending data to the database appear. One file turns into a 2000-line monster. Sound familiar?
This raises a question that torments every second beginner (and which has gathered thousands of views on Stack Overflow): how to properly organize a Go project with multiple files? Not just scatter code across folders, but do it so that it works, compiles, and doesn't cause errors like undefined or import cycle not allowed.
In this article, we won't just give a template. We'll look under the hood of the Go compiler and understand how the connection of files, packages, and modules actually works. You'll learn why go run *.go sometimes saves the day and sometimes doesn't, and how to build an architecture that scales.
File, Package, Module — Basic Hierarchy
\n\nBefore diving into code, let's break down three key concepts. Without them, organizing a Go project turns into guesswork.
\n\n- \n
- File (
.go) — is just a text file with code. By itself, it means nothing to the compiler. \n - Package (
package) — is a logical group of files located in the same directory. All files in a folder must have the samepackagedeclaration. If a folder contains files with different packages, the compiler will throw an error. \n - Module (
go.mod) — is the root of your project. It defines the module name, its dependencies, and Go versions. A module can contain many packages. \n
Golden rule: one directory = one package. The exception is tests, but more on that later.
\n\nPractical Example: "Log Analyzer" Project
\n\nLet's create a small project that reads a log file, filters lines by a keyword, and outputs the result. We'll do this first "the brute force way" (one file), and then split it into multiple files correctly.
\n\nStage 1: Everything in one file (bad)
\n\n// main.go\npackage main\n\nimport (\n "bufio"\n "fmt"\n "os"\n "strings"\n)\n\nfunc readLog(path string) ([]string, error) {\n file, err := os.Open(path)\n if err != nil {\n return nil, err\n }\n defer file.Close()\n\n var lines []string\n scanner := bufio.NewScanner(file)\n for scanner.Scan() {\n lines = append(lines, scanner.Text())\n }\n return lines, scanner.Err()\n}\n\nfunc filterLines(lines []string, keyword string) []string {\n var result []string\n for _, line := range lines {\n if strings.Contains(line, keyword) {\n result = append(result, line)\n }\n }\n return result\n}\n\nfunc main() {\n lines, err := readLog("server.log")\n if err != nil {\n fmt.Println("Error:", err)\n return\n }\n filtered := filterLines(lines, "ERROR")\n for _, l := range filtered {\n fmt.Println(l)\n }\n}\n\n\nIt works. But imagine that readLog becomes more complex (needs gzip support, encodings), and filterLines gets cluttered with regular expressions. The file will become unreadable.
Stage 2: Split into packages (correct)
\n\nLet's create the directory structure:
\n\nlog-analyzer/\n├── go.mod\n├── main.go\n└── pkg/\n └── logreader/\n ├── reader.go\n └── filter.go\n\n\nInitialize the module:
\n\ngo mod init log-analyzer\n\nNow let's write the code for the logreader package. Note: all files in the pkg/logreader/ folder have package logreader.
pkg/logreader/reader.go
\n\npackage logread