Scraping Central is reader-supported. When you buy through links on our site, we may earn an affiliate commission.

GO2beginner7 min read

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).
  • range gives 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]int that 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.

Go Syntax You Actually Need1 / 6

Which Go declaration form is the standard inside a function body?

Score so far: 0 / 0