Go for JavaScript Developers: Getting Started
I walk through Go's type system, variables, packages, the three-command toolchain, and multiple return values — with honest comparisons for developers who know Node.js.

The TypeScript team has a reasonable pitch: rename your file from .js to .ts and the program still runs. The type system is layered on top; nothing about runtime behavior changes until you want it to. Go offers no such gradual on-ramp. The first Go function you write, the compiler asks you to declare the type of every parameter, what the function returns, and whether you handled the error from the last function you called. Hand it an integer where a string belongs and it won’t run your program.
That sounds strict. What developers find after a few hours is that it’s also surprisingly honest. Go doesn’t hide decisions — it writes them into the code.
This is the opener of a four-part series on Go for developers who know JavaScript or TypeScript well. By the end you’ll have a working model of Go’s type system, its package structure, and the three go commands you use every day — enough to read unfamiliar Go code without feeling lost.
Series: Part 1 of 4. Each post stands alone.
On this page
- Why Go keeps showing up in JavaScript developers’ feeds
- A type system that doesn’t erase itself
- Variables: two declaration forms, one clear reason
- Packages, imports, and what replaces npm
- The toolchain in three commands
- Functions and the return value that surprises everyone
- Where the JavaScript mental model runs out first
- Go treats the compiler as your first code reviewer
Why Go keeps showing up in JavaScript developers’ feeds
When a Node.js API gets rewritten for performance, Go is usually in the conversation — often alongside Rust. When a backend service needs to handle tens of thousands of concurrent connections without per-request overhead, Go’s concurrency model comes up. The Stack Overflow Developer Survey 2024 places Go in the top ten most-wanted languages, despite being used by a much smaller share of day-to-day developers than JavaScript or TypeScript.
That gap between “wanted” and “used” is worth noticing. Go isn’t replacing JavaScript in browsers and it isn’t replacing TypeScript in most frontend or Node.js projects. What it does well is handle workloads that feel awkward in JavaScript: CLI tools, background workers, network services that need real concurrency.
If you’ve worked through the Node.js runtime fundamentals, you know that JavaScript’s single-threaded event loop is both its defining feature and its constraint. Everything runs on one thread, and concurrency comes from deferring work through callbacks, promises, and async/await — never from genuinely parallel execution. Go takes the opposite approach: concurrency is a first-class language primitive, and the language is designed around it. That’s the headline. The rest of this post covers the foundations you need to get there.
A type system that doesn’t erase itself
Go is statically typed. Every variable has a type known at compile time, enforced in the binary the compiler produces. That differs from TypeScript in a specific way: TypeScript types are erased before the code runs — they exist only during development and checking. Go’s types don’t disappear. They’re part of the compiled output.
The basic numeric and string types compare to JavaScript roughly like this:
| JavaScript | Go equivalent | Notes |
|---|---|---|
number (integer use) |
int, int64 |
Go distinguishes integer sizes |
number (decimal) |
float64 |
Most floating-point work uses this |
string |
string |
Same semantics, UTF-8 by default |
boolean |
bool |
true / false, same as JS |
Go also has int8, int16, int32, uint variants, and float32, but int and float64 cover most everyday use. The compiler won’t implicitly convert between them — you cast explicitly.
Structs replace plain objects as the primary way to group related data:
type User struct {
ID int
Name string
Email string
}
There are no classes. Structs hold data; methods attach to types and live outside the struct definition. If you’ve used TypeScript interfaces for object shapes, structs feel familiar. The behavior is close enough that most TypeScript developers find Go struct definitions readable on first contact.
One thing that caught my attention: every type has a zero value — 0 for integers, "" for strings, false for booleans, nil for pointers and interfaces. There’s no undefined. Declare a variable without assigning it and you always get the zero value. That guarantee is something JavaScript and TypeScript don’t offer, and it makes Go code surprisingly predictable once you’re used to it.
Variables: two declaration forms, one clear reason
You’ll see two ways to declare variables in Go:
// Long form — works at package level and inside functions
var name string = "Soumitra"
var count int = 0
// Short declaration — only valid inside a function body
name := "Soumitra"
count := 0
The := operator infers the type from the right side. Inside a function, it’s what you’ll see most of the time — it’s shorter and the intent is clear. At the package level (outside any function), you must use var.
Two Go rules that enforce tidiness in a way JavaScript doesn’t:
Unused variables don’t compile. Declare a variable and never read it, and Go refuses to build the program. Not a warning — an error.
Unused imports don’t compile. Import a package and never reference it, and the build fails. Combined with the unused-variable rule, this means Go codebases can’t accumulate dead imports the way JavaScript projects sometimes do over time.
I found both rules annoying for about a week. After that, I appreciated them — a codebase that compiles is a codebase with no dead weight baked in by the compiler.
Packages, imports, and what replaces npm
Every Go file begins with a package declaration:
package main
The special name main marks the entry point of an executable program. Library code uses a descriptive name — package auth, package database. All files in the same directory share a package name.
Imports use the module path plus the subdirectory:
import (
"fmt"
"net/http"
"encoding/json"
)
fmt, net/http, and encoding/json are part of Go’s standard library. The standard library ships with Go and covers HTTP servers, JSON encoding, cryptography, testing, file I/O, and more. Unlike npm, you often don’t need a third-party package for common tasks.
External packages are managed through modules. You initialize a module once:
go mod init github.com/yourname/projectname
That creates a go.mod file — the rough equivalent of package.json. It records your module name and the Go version. When you import a package from outside the standard library and run go mod tidy, Go fetches the package, pins the version in go.mod, and writes a checksum to go.sum.
The visibility rule that always trips JavaScript developers: in Go, an identifier is exported from a package if and only if it starts with an uppercase letter. fmt.Println is exported. fmt.println would be unexported and inaccessible from outside the package. There’s no export keyword, no module.exports, no default exports. Upper vs. lower case is the entire visibility system.
The toolchain in three commands
Node.js and Go have very different toolchain models. With Node.js you have node, npm, a bundler, possibly a transpiler, and a dev server. With Go, one command does almost everything:
go run main.go
Compiles and executes the file directly. Nothing is written to disk. Good for experiments and short scripts.
go build ./...
Compiles the module and writes a native binary. The ./... pattern means “this package and all packages inside it recursively.” The binary runs without a runtime installed on the target machine — you copy it and run it.
go install github.com/yourname/tool
Downloads, compiles, and places the binary in your $GOPATH/bin. This is how you install CLI tools written in Go. After go install, the tool is available globally.
There are also go test ./... (runs all tests), go fmt ./... (formats every file according to Go’s standard style — there’s no .prettierrc equivalent because there’s nothing to configure), and go vet (flags suspicious code patterns). The whole development cycle lives inside the go command.
Functions and the return value that surprises everyone
Go function syntax is readable once you’ve seen it a few times:
func greet(lang string, version int) string {
return lang + " " + fmt.Sprintf("v%d", version)
}
Parameters have types after the name; the return type follows the closing parenthesis. That’s familiar enough from TypeScript.
What’s different: Go functions routinely return two values — the result and an error. This is the standard pattern for any operation that can fail:
func parsePort(raw string) (int, error) {
port, err := strconv.Atoi(raw)
if err != nil {
return 0, fmt.Errorf("invalid port %q: %w", raw, err)
}
if port < 1 || port > 65535 {
return 0, fmt.Errorf("port %d out of valid range", port)
}
return port, nil
}
The caller handles the error explicitly — every time, at the call site:
port, err := parsePort("8080")
if err != nil {
log.Fatal(err)
}
// port is now a valid int — the compiler knows this
There’s no try/catch. Errors are regular values returned through the normal return channel. If you’ve worked on a Node.js Express API and hit a case where an error was thrown three functions deep and caught somewhere unexpected, Go’s pattern will feel verbose at first and then intentional. Every failure path is visible at the call site. You can read a function and know exactly where it can fail.
The nil return for error is how Go signals success: return (result, nil) when everything worked, (zeroValue, err) when it didn’t.
Where the JavaScript mental model runs out first
The place JavaScript thinking stops mapping cleanly to Go is concurrency.
Go’s main concurrency primitive is the goroutine. Starting one looks almost trivial:
go processOrder(orderID)
That go keyword before the function call runs it in a new goroutine — a lightweight unit of execution managed by the Go runtime. The Go spec documents that goroutines start with small stacks that grow as needed; in practice you can start hundreds of thousands of them in a single program without the overhead of OS threads.
But goroutines aren’t the hard part. Communicating between them is where the model changes. Go uses channels to pass values between goroutines:
results := make(chan string)
go func() {
results <- fetchFromDatabase(id)
}()
data := <-results
The <- operator sends to or receives from a channel. A receive blocks until a value is available. A send blocks until something receives. This is Go’s synchronization model: instead of shared memory guarded by locks, you pass values through typed pipes.
The JavaScript event loop processes one queued task at a time, using promises and callbacks to defer work without blocking. Go runs multiple goroutines in parallel on all available CPU cores, coordinated through channels. The same goal — “do work without blocking the main thread” — looks genuinely different in each language, and one approach doesn’t cleanly map onto the other.
The next post in this series goes deeper into goroutines and channels with practical examples. For now, the important thing to carry forward is that concurrency in Go is a language feature, not a workaround for a single-threaded runtime.
Go treats the compiler as your first code reviewer
The practical effect of Go’s static types, explicit errors, and enforced-clean imports is this: many mistakes that would only surface at runtime in JavaScript surface at compile time in Go.
A type mismatch is a build error. An unhandled error return isn’t a runtime explosion — it’s a compile warning you can make into a linter error. Passing the wrong number of arguments to a function doesn’t just throw at runtime; it doesn’t compile. The compiler reads your program before you run it and raises objections.
If you’ve already used TypeScript seriously, you’ll recognize the direction — catching class of errors before execution rather than during it. Go takes that further. TypeScript’s types are erased; the runtime knows nothing about them. Go’s types survive compilation and are enforced in the binary itself.
The trade-off is real. Go’s syntax is new. The error handling is repetitive at first. The lack of generics in older codebases leads to patterns that feel clunky to TypeScript developers. None of that disappears on first contact.
But the core shift — from “the program tells me what’s wrong when it runs” to “the compiler tells me what’s wrong before it runs” — is the same shift TypeScript already made worthwhile. Go just takes it further down the stack.