Introduction
Let's write a small Go program that prints a message for an article we want to publish. We'll run it during development, split its code across two files, and build an executable we can run without invoking the Go tool.
If you already write PHP, functions and conditionals need little introduction. The useful questions are about the differences. Where does execution start? How does one file use a function from another? Why does an unused import stop the build?
This is the first article in Go for PHP Developers, a series that uses familiar PHP concepts to explain Go. I want to keep those comparisons precise. A Go package is not a PHP namespace, and later we'll see why a pointer is not a PHP reference.
We'll use a small article-publishing application as a continuing example. Today it only prints a message. Storage, HTTP endpoints, and concurrent work can wait until we have the language concepts needed to explain them.
Install Go and Check the Toolchain
Install Go 1.27.4 using the official installation instructions for your operating system. This is the latest stable version listed in the Go downloads feed when this article was checked on November 13, 2026.
Open a terminal and check the installation:
go version
On Linux with an AMD64 processor, the output is:
go version go1.27.4 linux/amd64
The operating system and architecture at the end depend on your machine. If the command
is missing, check the installation instructions for adding Go's executable directory to
your PATH, then open a new terminal.
The examples below use Go 1.27.4 and only its standard library. We don't need a web framework or any third-party packages.
Create a Module
Create an empty directory for the example and enter it. These commands use a Unix shell, as do the executable commands later in the article:
mkdir publishing
cd publishing
go mod init example.com/publishing
The last command reports:
go: creating new go.mod: module example.com/publishing
A module groups Go packages that are versioned together. Its go.mod file records
the module path, the Go version requirement, and dependency requirements when we add them.
For this example, the generated file contains:
module example.com/publishing
go 1.27.4
This block is the contents of go.mod, not Go source code.
If you use Composer, dependency metadata is a familiar responsibility. The comparison
stops short of autoloading. A Go module does not configure PHP-style class autoloading,
and go.mod is not a replacement for both composer.json and composer.lock.
example.com/publishing is a placeholder module path for this local exercise. You don't
need to own that domain or create a remote repository. For a module you intend others
to download, choose a path that Go's tools can resolve, such as its repository location.
The module naming guidance
explains those requirements.
Write and Run the First Program
Create main.go beside go.mod:
package main
import "fmt"
func main() {
fmt.Println("Ready to publish: Learning Go")
}
Run the package in the current directory:
go run .
You should see:
Ready to publish: Learning Go
For a PHP command line script, you can put executable statements at the top level of a
file and invoke that file with php. Our Go executable instead has a main function
in a package named main. After package initialisation, execution enters that function.
The filename main.go is a convention here. The function and package names establish
the entry point.
fmt is a standard-library package for formatted input and output. Println writes
its arguments followed by a newline to standard output. The capital P also matters.
In Go, an identifier that starts with an uppercase letter can be exported from a package.
We'll return to visibility when we organise our own packages.
Notice that go run . names a package location, rather than a particular source file.
The tool compiles that package and runs the resulting program. It does not leave a
named executable in our project directory for us to distribute.
Build an Executable You Can Run Again
Build the same program with an explicit output name:
go build -o publisher .
./publisher
The output is the same:
Ready to publish: Learning Go
On Windows, use go build -o publisher.exe . and run .\publisher.exe in PowerShell.
The two development paths look like this:
Go source files
|
+-- go run . ----------------- compile, then run
|
+-- go build -o publisher . -- create ./publisher
|
+-- ./publisher runs the built program
Both commands compile the source. The difference is whether we ask the tool to run the program now or produce a named executable we can run later.
For this standard-library example, the built executable runs without a Go installation. That does not mean every Go application is independent of its environment. Programs can still need configuration, certificates, data files, or native libraries. The executable also targets an operating system and architecture.
Change the message in main.go, save it, and run ./publisher again. It still prints
the old message. Rebuild it to include the changed source, or use go run . to compile
and run the current source during development.
I would use go run . while working through these examples and build a named executable
when I want to run that specific build again.
Put Related Code in Another File
Let's separate constructing the message from printing it. This gives us a small function we can test without capturing terminal output.
Replace main.go with:
package main
import "fmt"
func main() {
title := "Learning Go"
fmt.Println(publicationMessage(title))
}
Create message.go in the same directory:
package main
func publicationMessage(title string) string {
return "Ready to publish: " + title
}
Run the package again:
go run .
The output remains Ready to publish: Learning Go.
There is no require, autoload registration, or import of message.go. Both source files
belong to the same package in the same directory, so main can call publicationMessage.
The lowercase function name keeps it unexported from the package, while leaving it
available to other files in that package.
Imports belong to each source file. If message.go needed fmt, it would need its own
import even though main.go already imports that package.
This is one reason to prefer go run . here. Running go run main.go builds from that
explicit file list and leaves out message.go, so the call to publicationMessage
fails to compile. You can explicitly name both files, but running the package avoids
maintaining that list as it grows.
The function declaration also shows a syntax difference worth reading slowly.
title string declares a parameter named title with type string. The string
after the closing parenthesis declares the return type. In PHP, you would put the
parameter type before its name and the return type after a colon.
title := "Learning Go" declares a local variable whose type is inferred as string.
Type inference does not make the variable dynamically typed. It still has a type
that the compiler checks.
Read Compiler Errors as Part of the Workflow
Try changing the function call in main.go to publicationMessage(42), then run
go run .. This is a deliberately invalid change.
The compiler reports that it cannot use 42 as a string argument. It catches that mismatch
before executing our application. Restore publicationMessage(title) afterward.
There is another common first encounter with the compiler. If you add an import and
never use it in that file, Go rejects the file. Declared local variables must also be
used, with the blank identifier _ available when deliberately discarding a value.
These checks are part of compilation, rather than optional editor warnings.
I wouldn't try to memorise every diagnostic now. Read the filename and line number, check the specific declaration or call, and make one correction. A successful build still says nothing about whether our message is the one we wanted.
Check the Message With a Test
Create message_test.go:
package main
import "testing"
func TestPublicationMessage(t *testing.T) {
got := publicationMessage("Learning Go")
want := "Ready to publish: Learning Go"
if got != want {
t.Fatalf("publicationMessage() = %q, want %q", got, want)
}
}
Run the tests:
go test ./...
The result includes ok followed by example.com/publishing. The timing varies.
The ./... pattern selects packages in the current directory and below it within
the applicable module boundaries.
Go's testing tools discover test functions in files ending with _test.go. The test
uses the same package as our implementation, which lets it call the unexported function.
The testing package is part of the standard library, and t.Fatalf reports the
mismatch and stops that test.
Change want to a different message and rerun the command. The test should fail and show
both strings. Restore the expected value and run it again to get a passing test.
We are checking message construction only. This test doesn't validate publication, persist anything, or prove that our command printed to the terminal. Our application doesn't implement publication yet.
Format the Code With the Toolchain
Format the package:
go fmt ./...
The command prints the names of files it changes. No output is normal when the files are already formatted. Go supplies a standard formatter, so we can let the tool handle indentation instead of choosing a formatting style for every project.
Our directory now contains:
publishing/
go.mod
main.go
message.go
message_test.go
publisher
publisher is the executable created earlier. The test file participates in go test,
but is excluded from the ordinary executable build.
From this directory, the commands for checking and building the example are:
go fmt ./...
go test ./...
go build -o publisher .
./publisher
There are no external dependencies to install. The official getting-started tutorial and module layout guide provide more background on the toolchain and this project structure.
Where the PHP Comparison Stops
PHP knowledge gives us useful questions, but several concepts need their own Go explanation. Packages organise declarations and access across source files. Modules group packages and record dependency requirements. Neither should be treated as a renamed PHP namespace.
The command line comparison is also deliberately narrow. PHP has command line tools and long-running workers, so a persistent process is not unique to Go. When we reach HTTP, we'll compare the actual server lifecycles rather than assume every PHP application uses the same request model.
I would keep the first version of this project in one package. We have two source files and one test. There is no reason to introduce controller directories, repositories, or a dependency injection container before the application needs those boundaries.
Conclusion
Try changing the article title, running the package, and rebuilding the executable. Then make the test fail intentionally and use its output to find the mismatch. Those exercises give you a way to check your understanding before we add more language features.
Next in Go for PHP Developers, we'll work through types, zero values, and nil, using PHP type declarations as our starting point.
I hope that you liked this article and if you do, don't forget to share this article with your friends!!! See ya!