Go Syntax You Actually Need
A weekend's worth of Go: variables, types, structs, error handling, slices, maps. Enough to read scraping tools, not enough to write a backend service.
What you’ll learn
- Read any Go file in a scraping tool without reaching for a dictionary.
- Write small Go programs with structs, slices, maps, and standard error handling.
- Recognise Go's idiomatic patterns: short variable declarations, multiple return values, `if err != nil`.
- Skip what you don't need: generics, interfaces beyond basics, advanced concurrency primitives.
The goal of this lesson is to get you reading Go in two evenings. Not writing it fluently; reading it. You'll write a little along the way, but the test is: open tls-client/main.go and follow what's happening.
If you've used C, Java, or any statically-typed language, this will feel familiar. If you only know Python, the surprises are: explicit types, no exceptions, no overloading, no inheritance.
Hello World, with the structure
package main // every file declares a package
import "fmt" // imports go in parens or single-line
func main() {
fmt.Println("Hello, scraper")
}
Save as hello.go, run go run hello.go. Every Go program starts with package main and a func main().
fmt is the standard library's print package. Capitalised names (Println) are exported (public); lowercase names (println) are package-private. This is Go's only access control.
Variables and types
// Explicit type and value.
var name string = "Catalog108"
var port int = 443
// Type inferred from value.
var url = "https://practice.scrapingcentral.com/"
// Short declaration inside a function (most common form).
count := 0
host, scheme := "example.com", "https"
:= is "declare and infer type". Use it inside functions. var is for package-level declarations or when you want to be explicit.
Common types you'll meet:
| Type | What it is |
|---|---|
string |
UTF-8 string, immutable |
int, int32, int64 |
Integer (int is platform-sized) |
float64 |
The default float |
bool |
true/false |
[]byte |
Byte slice, the way Go does "bytes" (vs string) |
[]T |
Slice of T |
map[K]V |
Hash map |
struct {...} |
Composite type, like a Python dataclass |
interface{...} or any |
Empty interface (Go 1.18+ uses any as alias) |
Slices, the Go equivalent of Python lists
A "slice" in Go is a view into an array. For scraper purposes, treat it like Python's list.
urls := []string{
"https://example.com/",
"https://example.org/",
}
urls = append(urls, "https://example.net/") // grow
fmt.Println(len(urls)) // 3
fmt.Println(urls[0]) // first
for i, url := range urls {
fmt.Printf("%d: %s\n", i, url)
}
Key idioms:
append(slice, item)returns a new slice (possibly), always re-assign:urls = append(urls, ...).len(s)is the length.s[1:3]is a sub-slice from index 1 (inclusive) to 3 (exclusive).rangegives you index + value.
Maps, the Go equivalent of Python dicts
counts := map[string]int{
"200": 0,
"404": 0,
}
counts["200"]++
counts["500"] = 1
if v, ok := counts["404"]; ok {
fmt.Println("404 count:", v)
}
for status, n := range counts {
fmt.Printf("%s: %d\n", status, n)
}
The v, ok := m[key] form is how you check membership without confusing "missing" with "zero". This is the Go equivalent of Python's d.get(key) or key in d.
Structs, the dataclass equivalent
type Product struct {
SKU string
Title string
Price float64
}
p := Product{SKU: "abc-123", Title: "Mouse", Price: 19.99}
fmt.Println(p.SKU, p.Title, p.Price)
// You can also create with positional args (less safe):
p2 := Product{"xyz-456", "Keyboard", 49.99}
Struct fields are capitalised when exported (visible outside the package). Methods are attached to structs like this:
func (p Product) DisplayPrice() string {
return fmt.Sprintf("$%.2f", p.Price)
}
fmt.Println(p.DisplayPrice()) // "$19.99"
The (p Product) is the receiver, Go's version of self. Use *Product (pointer receiver) if the method needs to mutate the struct or if the struct is large enough that copying matters.
Errors, the if err != nil pattern
Go has no exceptions. Functions that can fail return an error as their last return value. You check it explicitly.
import (
"errors"
"fmt"
"net/http"
)
func fetch(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("get %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return nil, errors.New("non-200 response")
}
return io.ReadAll(resp.Body)
}
func main() {
body, err := fetch("https://example.com/")
if err != nil {
fmt.Println("fetch failed:", err)
return
}
fmt.Println(string(body))
}
You will see if err != nil { return ..., err } everywhere in Go code. It looks repetitive; that's the trade for explicit error handling and no surprise exceptions.
The defer statement schedules a call to run when the function returns. It's how Go closes files, connections, locks, etc. always.
fmt.Errorf and %w for error wrapping
return fmt.Errorf("fetching %s: %w", url, err)
The %w verb wraps the underlying error so callers can unwrap it with errors.Is and errors.As. Use %w when you want callers to be able to test the underlying error type; use %v when you just want a string.
Pointers, the lighter version
Go has pointers but no pointer arithmetic. Two uses:
- Mutate through a pointer. If a method needs to change the struct, use a pointer receiver.
- Avoid copying. Large structs are cheap to pass by pointer.
func (p *Product) Discount(percent float64) {
p.Price *= (1 - percent) // mutates the original
}
p := Product{SKU: "abc", Price: 100}
p.Discount(0.10) // p.Price is now 90
&x takes the address of x. *p dereferences p. In practice for scraping work, you'll write func (x *T) ... for mutating methods and not think about pointers much beyond that.
Interfaces (just the basics)
An interface is a set of method signatures. Any type with those methods satisfies the interface, no explicit "implements" keyword.
type Fetcher interface {
Fetch(url string) ([]byte, error)
}
// Anything with a Fetch method satisfies Fetcher.
The two interfaces every Go programmer meets in their first week:
io.Reader, anything you can.Read(p []byte) (n int, err error)from. HTTP response bodies, files, network connections.io.Writer, the symmetric write side.
These two power Go's "everything is a stream" model. Don't worry about defining your own interfaces yet; just recognise them when you see them.
What to skip
For the depth target of this sub-path:
- Generics (
func Map[T, U any](...)). Useful, but you can read 95% of Go code without them. - Channels-as-types beyond the basics in GO3.
- Reflection (
reflect.TypeOf,reflect.Value). Almost never appears in scraping tools. - Build tags, CGo, assembly. Out of scope.
- Go modules deep dive.
go mod init,go get,go build, that's enough.
A minimal scraping snippet putting it together
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
type Result struct {
URL string
Title string
Status int
}
func fetch(url string) (Result, error) {
resp, err := http.Get(url)
if err != nil {
return Result{}, fmt.Errorf("get: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return Result{}, err
}
title := extractTitle(string(body))
return Result{URL: url, Title: title, Status: resp.StatusCode}, nil
}
func extractTitle(html string) string {
start := strings.Index(html, "<title>")
end := strings.Index(html, "</title>")
if start == -1 || end == -1 {
return ""
}
return html[start+7 : end]
}
func main() {
urls := []string{
"https://practice.scrapingcentral.com/",
"https://example.com/",
}
for _, url := range urls {
r, err := fetch(url)
if err != nil {
fmt.Printf("ERR %s: %v\n", url, err)
continue
}
fmt.Printf("OK %s (%d): %s\n", r.URL, r.Status, r.Title)
}
}
Save as mini.go, go run mini.go. Every syntax pattern in this lesson is in those 40 lines. Read it twice.
Where to practice
- Take the snippet above and add a
Result.PrettyPrint()method. - Add a
map[string]intthat counts how many URLs returned 200 vs not-200. - Run
go vet ./...(the linter) on your code. It will catch unused imports and other common slips. - The Go Tour takes ~3 hours end to end and is the most cost-effective way to deepen anything in this lesson.
Next: GO3 is where Go starts being genuinely interesting for scraping, goroutines and channels.
Quiz, check your understanding
Pass mark is 70%. Pick the best answer; you’ll see the explanation right after.