{
    "archive_path": "archive/1790503447.653855",
    "base_url": "antonz.org/go-concurrency-distilled",
    "basename": "",
    "bookmarked_date": "2026-09-27 10:04",
    "canonical": {
        "archive_org_path": "https://web.archive.org/web/antonz.org/go-concurrency-distilled",
        "dom_path": "output.html",
        "favicon_path": "favicon.ico",
        "git_path": "git/",
        "google_favicon_path": "https://www.google.com/s2/favicons?domain=antonz.org",
        "headers_path": "headers.json",
        "htmltotext_path": "htmltotext.txt",
        "index_path": "index.html",
        "media_path": "media/",
        "mercury_path": "mercury/content.html",
        "pdf_path": "output.pdf",
        "readability_path": "readability/content.html",
        "screenshot_path": "screenshot.png",
        "singlefile_path": "singlefile.html",
        "warc_path": "warc/",
        "wget_path": "antonz.org/go-concurrency-distilled/index.html"
    },
    "domain": "antonz.org",
    "extension": "",
    "hash": "EBY6DGGFEGW2NF46VPE1",
    "history": {
        "archive_org": [],
        "dom": [],
        "favicon": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=antonz.org"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T13:38:11.368835+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:10.728685+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=antonz.org"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T10:09:48.366796+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:47.723091+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--output",
                    "favicon.ico",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://www.google.com/s2/favicons?domain=antonz.org"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T16:27:29.248773+00:00",
                "index_texts": null,
                "output": "favicon.ico",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:28.896327+00:00",
                "status": "succeeded"
            }
        ],
        "git": [],
        "headers": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T13:38:11.729889+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:11.374879+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T10:09:48.711291+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:48.371794+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--head",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T16:27:29.796825+00:00",
                "index_texts": null,
                "output": "headers.json",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:29.254419+00:00",
                "status": "succeeded"
            }
        ],
        "htmltotext": [],
        "media": [],
        "mercury": [
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-09-27T13:38:25.362599+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:18.096042+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-09-27T10:10:02.011151+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:55.055915+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "/app/node_modules/@postlight/parser/cli.js",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "1.0.0",
                "end_ts": "2026-09-27T16:27:41.891734+00:00",
                "index_texts": null,
                "output": "mercury",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:35.713889+00:00",
                "status": "succeeded"
            }
        ],
        "pdf": [],
        "readability": [
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmpau7yzcof",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-09-27T13:38:18.084306+00:00",
                "index_texts": [
                    "Go concurrency distilledThis mini-book provides a brief overview of many concurrency topics in Go. Each topic comes with interactive examples \u2014 feel free to experiment with them by changing the code and clicking Run. There's also a PDF version with static examples.This is a quick refresher on Go concurrency, not a beginner's guide. If you want to learn concurrency from the ground up with practical exercises, check out my other book \u2014 Gist of Go: Concurrency.The book is AI-free.Goroutines\u00a0\u2022\nChannels\u00a0\u2022\nSelect\u00a0\u2022\nPipelines\u00a0\u2022\nTime\u00a0\u2022\nContext\u00a0\u2022\nWait groups\u00a0\u2022\nData races\u00a0\u2022\nRace conditions\u00a0\u2022\nMutexes\u00a0\u2022\nSemaphores\u00a0\u2022\nSignaling\u00a0\u2022\nRun once\u00a0\u2022\nObject pool\u00a0\u2022\nAtomics\u00a0\u2022\nTesting\u00a0\u2022\nScheduling\u00a0\u2022\nDiagnostics\u00a0\u2022\nFinal thoughts#\nGoroutinesThe foundation of concurrency in Go is goroutines \u2013 functions started with the go keyword:func main() {\n    var wg sync.WaitGroup\n    wg.Add(2)\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 1\")\n    }()\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 2\")\n    }()\n    wg.Wait()\n}\nThe Go runtime juggles these goroutines and distributes them among operating system threads running on CPU cores. Compared to OS threads, goroutines are lightweight, so you can create hundreds or thousands of them.Goroutines are completely independent. The main function is also a goroutine, but it starts implicitly when the program starts. When main ends, other goroutines also shut down.We use a wait group (sync.WaitGroup) to wait for goroutines to finish in the example above. A wait group has a counter inside. Calling Add(n) increments it by n, while Done() decrements it by one. Wait() blocks the calling goroutine (in this case, main) until the counter reaches zero. This way, main waits for both workers to finish before it exits.WaitGroup.Go automatically increments the wait group counter, runs a function in a goroutine, and decrements the counter when it's done:func main() {\n    var wg sync.WaitGroup\n    wg.Go(func() {\n        fmt.Println(\"worker 1\")\n    })\n    wg.Go(func() {\n        fmt.Println(\"worker 2\")\n    })\n    wg.Wait()\n}\n#\nChannelsGoroutines can pass values to each other through channels. A channel is like a window where one goroutine can throw something and another can catch it:func main() {\n    messages := make(chan string)\n\n    go func() { messages <- \"ping\" }()\n\n    msg := <-messages\n    fmt.Println(msg)\n}\nSending a value through a channel is a synchronous operation. When the sending goroutine writes a value to the channel (ch <- val), it blocks and waits for someone to receive that value (<-ch). Only then does it continue.Output channelReturning an output channel from a function and filling it within an internal goroutine is a common pattern in Go. This allows the caller to receive values through the channel while the owning function retains control of it:func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nClosing a channelTo signal readers that all data has been sent, the writer goroutine closes the channel with close():func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nThe reader checks the channel's status with a second value (\"comma OK\") when reading:func main() {\n    in := generate(5, 10)\n    for {\n        num, ok := <-in\n        if !ok {\n            break\n        }\n        fmt.Print(num, \" \")\n    }\n}\nWhile the channel is open, the reader receives the next value and a true status. If the channel is closed, the reader gets a zero value and a false status.A channel can only be closed once. Closing it again or writing to a closed channel causes a panic.The only reason to close a channel is to signal to its readers that all data has been sent. If this isn't important to the readers, then you don't need to close it. When a channel is no longer used, Go's garbage collector will free its resources, whether it's closed or not.Channel iterationrange automatically reads the next value from the channel and checks if it's closed. If the channel is closed, it exits the loop:func main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nRange over a channel returns a single value, not a pair, unlike range over a slice.Directional channelsYou can protect yourself from accidental write/close errors by setting the channel direction. Channels can be:chan (bidirectional): for reading and writing (default);chan<- (send-only): for writing only;<-chan (receive-only): for reading only.You can't read from a send-only channel or write to a receive-only channel (nor can you close it).Channels are usually initialized for both reading and writing, and specified as directional in function parameters. Go automatically converts a regular channel to a directional one:stream := make(chan int)\n\ngo func(in chan<- int) {\n    in <- 42\n}(stream)\n\nfunc(out <-chan int) {\n    fmt.Println(<-out)\n}(stream)\nBuffered channelsBuffered channels work like a FIFO queue with a fixed-size buffer for storing values.As long as the buffer has free space, writing to the channel doesn't block the goroutine. Similarly, as long as the buffer contains values, reading from the channel doesn't block the goroutine:stream := make(chan int, 3)\nstream <- 11\nstream <- 12\nstream <- 13\n\nfmt.Println(<-stream)\nfmt.Println(<-stream)\nBy default, if you don't specify a buffer size, a channel is unbuffered (buffer size equals zero).Buffered channels work with the built-in len() and cap() functions:stream := make(chan int, 3)\nstream <- 11\nfmt.Println(cap(stream), len(stream))\nReading from a closed buffered channel returns values from the buffer and a true status. Once all values are taken, it returns a zero value and a false status, like a regular channel:stream := make(chan int, 1)\nstream <- 11\nclose(stream)\n\nval, ok := <-stream\nfmt.Println(val, ok)\n// 11 true\n\nval, ok = <-stream\nfmt.Println(val, ok)\n// 0 false\nnil channelLike any type in Go, channels have a zero value, which is nil.Writing to or reading from a nil channel blocks the goroutine indefinitely:var stream chan int\n\ngo func() {\n    // blocks forever\n    stream <- 1\n}()\n\n// blocks forever\n<-stream\nClosing a nil channel causes a panic:var stream chan int\nclose(stream)\n// panic: close of nil channel\n#\nSelectThe select statement is somewhat like switch, but specifically designed for channels. Here's what it does:Checks which cases are not blocked.If multiple cases are ready, randomly selects one to execute.If all cases are blocked and there is a default case, executes it.If all cases are blocked and there is no default case, waits until one is ready.Select is used to manage data flow in pipelines:// merge sends values from in1 and in2 to the output channel.\nfunc merge(in1, in2 <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for in1 != nil || in2 != nil {\n            select {\n            case val1, ok := <-in1:\n                if ok { out <- val1 } else { in1 = nil }\n            case val2, ok := <-in2:\n                if ok { out <- val2 } else { in2 = nil }\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send 10..12 to in1, 20..22 to in2,\n// and call merge(in1, in2)\nTo cancel goroutines:// process modifies values from in and send them to out\n// until in is exhausted or cancel is closed.\nfunc process(cancel chan struct{}, in <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        for val := range in {\n            select {\n            case out <- val*10:\n            case <-cancel:\n                fmt.Println(\"canceled\")\n                return\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send values 11 and 12 to in\n// and then call close(cancel)\nFor non-blocking operations:// multiplier returns a function that multiplies\n// the input by 10 and sends it to the channel\n// or returns an error if the channel is busy.\nfunc multiplier(ch chan<- int) func(n int) error {\n    return func(n int) error {\n        select {\n        case ch <- n*10:\n            return nil\n        default:\n            return errors.New(\"busy\")\n        }\n    }\n}\n\nfunc main() {\n    nums := make(chan int, 1)\n    multiply := multiplier(nums)\n\n    err := multiply(11)\n    fmt.Println(<-nums, err)\n    // 110 <nil>\n\n    err = multiply(12)\n    fmt.Println(<-nums, err)\n    // 120 <nil>\n\n    err = multiply(13)\n    err = multiply(14)\n    fmt.Println(err)\n    // busy\n}\nAnd for much more.#\nPipelinesA pipeline is a sequence of operations where each step takes input data, processes it in a specific way, and outputs it. The input and output of each operation is a channel.A typical pipeline looks like this:Reader: Reads input data from a file, database, or network.N processors: Transform, filter, aggregate, or enrich data using external sources.Writer: Writes the processed data to a file, database, or network.func read[T any]() <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for {\n            // read data from somewere\n            data := // ...\n            out <- data\n        }\n    }()\n    return out\n}\n\nfunc process[T any](in <-chan T) <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for inData := range in {\n            // process the data\n            outData = // ...\n            out <- outData\n        }\n    }()\n    return out\n}\n\nfunc write[T any](in <-chan T) <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        for data := range in {\n            // write the data\n        }\n    }()\n    return done\n}\nOutput channelA goroutine can signal other goroutines that it has finished its work using an output channel:func generate(start, stop int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nDone channelIf a goroutine doesn't need to return results, it can signal completion using a done channel:func work() <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fmt.Println(\"work done\")\n    }()\n    return done\n}\n\nfunc main() {\n    done := work()\n    <-done\n}\nCancel channelTo terminate a goroutine early, a calling goroutine can use a cancel channel:func generate(cancel chan struct{}, n int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := 1; i <= n; i++ {\n            select {\n            case out <- i:\n            case <-cancel:\n                return\n            }\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    cancel := make(chan struct{})\n    defer close(cancel)\n\n    nums := generate(cancel, 10)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n}\nError handlingThere are three approaches to error handling in concurrent pipelines.\u278a Return on the first error:// calculate produces answers for the given numbers.\nfunc process(in <-chan int) (<-chan int, <-chan error) {\n\tout := make(chan Answer)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err != nil {\n\t\t\t\terrc <- err  // return with error\n\t\t\t\treturn\n\t\t\t}\n\t\t\tout <- ans\n\t\t}\n\t\terrc <- nil          // return with nil\n\t}()\n\treturn out, errc\n}\n\u278b Use a result type:// Result contains an answer or an error.\ntype Result struct {\n\tanswer int\n\terr    error\n}\n\n// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int) <-chan Result {\n\tout := make(chan Result)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tout <- Result{ans, err}  // return answer + error\n\t\t}\n\t}()\n\treturn out\n}\n\u278c Collect errors separately:// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int, errc chan<- error) <-chan int {\n\tout := make(chan Answer)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err == nil {\n\t\t\t\tout <- ans   // send answer\n\t\t\t} else {\n\t\t\t\terrc <- err  // or error\n\t\t\t}\n\t\t}\n\t}()\n\treturn out\n}\n#\nTimeBesides handling date and time, the time package offers tools for managing time-sensitive operations in concurrent programs.Aftertime.After() returns a channel that is initially empty, but receives a value after the timeout period. It's useful for timing out operations:// withTimeout executes a function with a given timeout.\nfunc withTimeout(timeout time.Duration, fn func()) error {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fn()\n    }()\n\n    // blocks until fn completes or the timer expires,\n    // whichever happens first\n    select {\n    case <-done:\n        return nil\n    case <-time.After(timeout):\n        return errors.New(\"timeout\")\n    }\n}\nwithTimeout() waits for fn() to complete, but thanks to time.After(), it won't wait longer than the timeout duration:func main() {\n    var err error\n\n    // completes in time\n    err = withTimeout(\n        50*time.Millisecond,\n        func() { fmt.Println(\"work done\") },\n    )\n    fmt.Println(\"err =\", err)\n\n    // gets canceled on timeout\n    err = withTimeout(\n        50*time.Millisecond,\n        func() {\n            time.Sleep(100 * time.Millisecond)\n            fmt.Println(\"work done\")\n        },\n    )\n    fmt.Println(\"err =\", err)\n}\nwork done\nerr = <nil>\nerr = timeout\nTimerA timer (time.Timer) is a structure with a C channel to which it sends the current time when it triggers (expires). Timers are useful for planning future executions:done := make(chan struct{})\n\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C  // blocks for 50ms\n    fmt.Println(\"work done at\", eventTime)\n    close(done)\n}()\n\n<-done\nwork done at 2009-11-10 23:00:00.05\nStop() stops the timer and returns true if it hasn't expired yet, and false otherwise:// timer expires after 50ms\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C\n    fmt.Println(\"work done at\", eventTime)\n}()\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n} else {\n    fmt.Println(\"too late to cancel\")\n}\nIt's often more convenient to use the time.AfterFunc() wrapper function. It waits for duration d and then executes function f:done := make(chan struct{})\nwork := func() {\n    fmt.Println(\"work done\")\n    close(done)\n}\n\n// executes work after 50ms\ntime.AfterFunc(50*time.Millisecond, work)\n<-done\ntime.AfterFunc() returns a timer that you can cancel before execution starts:// executes the function after 50ms\ntimer := time.AfterFunc(50*time.Millisecond, func() {})\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n}\nIf a timer is used in a loop, it's better to create a single timer and reset it instead of creating a new instance on each iteration:// consumer reads tokens from the input channel and alerts\n// if a value does not appear in a channel after an hour.\nfunc consumer(in <-chan token) {\n    const timeout = time.Hour\n    timer := time.NewTimer(timeout)\n    for {\n        timer.Reset(timeout)\n        select {\n        case <-in:\n            // do stuff\n        case <-timer.C:\n            // log warning\n        }\n    }\n}\n\n// Suppose we send 10,000 values to the in channel\n// and measure memory usage.\nMemory used: 4 KB, # allocations: 6\nTickerA ticker is like a timer, but it keeps firing until you stop it. Tickers are useful for executing periodic tasks:// fires every 50ms\nticker := time.NewTicker(50 * time.Millisecond)\ndefer ticker.Stop()\n\ngo func() {\n    for {\n        // waits for ticker to fire on each iteration\n        at := <-ticker.C\n        fmt.Println(\"work done at\", at)\n    }\n}()\n\n// enough time for the ticker to fire 3 times\ntime.Sleep(160*time.Millisecond)\nticker.Stop()\nwork done at 2009-11-10 23:00:00.05\nwork done at 2009-11-10 23:00:00.10\nwork done at 2009-11-10 23:00:00.15\nNewTicker(d) creates a ticker that sends the current time to the channel C at interval d. You must stop the ticker eventually with Stop() to free up resources.If the channel reader can't keep up with the ticker, the ticker will skip ticks.#\nContextThe main purpose of context is to cancel operations, either manually or by timeout/deadline.The function accepts a context and uses its Done() channel to listen for cancellation:// work performs a task for 50 ms unless canceled.\n// Returns an error when canceled.\nfunc work(ctx context.Context) error {\n    done := make(chan struct{})\n\n    go func() {\n        time.Sleep(50 * time.Millisecond)\n        fmt.Println(\"work done\")\n        close(done)\n    }()\n\n    select {\n    case <-done:\n        return nil\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}\nCancel manually (context.Canceled error):func main() {\n    // empty context\n    ctx := context.Background()\n    // manual canellation context\n    ctx, cancel := context.WithCancel(ctx)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    // cancels after 10 ms\n    time.Sleep(10 * time.Millisecond)\n    cancel()\n    <-done\n}\nCancel by timeout (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels after 10 ms\n    ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nCancel by deadline (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels at now + 10 ms\n    deadline := time.Now().Add(10 * time.Millisecond)\n    ctx, cancel := context.WithDeadline(ctx, deadline)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nContext is layered. A context object is immutable. To add new properties to a context, a new (child) context is created based on the old (parent) context. The shorter timeout between the parent and child contexts always wins. The child context can only shorten the parent's timeout, not extend it:func main() {\n    // parent context with a 100 ms timeout\n    const dur100ms = 100 * time.Millisecond\n    parentCtx, cancel := context.WithTimeout(context.Background(), dur100ms)\n    defer cancel()\n\n    // child context with a 10 ms timeout\n    const dur10ms = 10 * time.Millisecond\n    childCtx, cancel := context.WithTimeout(parentCtx, dur10ms)\n    defer cancel()\n\n    // now the work gets canceled\n    err := work(childCtx)\n    fmt.Println(\"err =\", err)\n}\nerr = context deadline exceeded\nMultiple cancels are safe. You can call cancel() on the context as many times as you want. The first cancel will work, and the rest will be ignored.You can specify a custom cancellation cause using context.WithCancelCause(), context.WithTimeoutCause() and context.WithDeadlineCause(). This cause is accessible through context.Cause():ctx, cancel := context.WithCancelCause(context.Background())\ncancel(errors.New(\"the night is dark\"))\nfmt.Println(context.Cause(ctx))\nYou can register a function to execute when the context is canceled with context.AfterFunc():ctx, cancel := context.WithCancel(context.Background())\ncleanup := func() { fmt.Println(\"cleanup\") }\ncontext.AfterFunc(ctx, cleanup)\ncancel()\ntime.Sleep(10 * time.Millisecond)\nContext can pass additional information about a call using context.WithValue(), which creates a context with a value for a specific key. But it's generally better to avoid passing values in context. It's better to use explicit parameters or custom structs instead.#\nWait groupsThe sync.WaitGroup type lets you wait for one or more goroutines to finish:const n = 10\nvar wg sync.WaitGroup\nwg.Add(n)\nfor range n {\n    go func() {\n        defer wg.Done()\n        fmt.Print(\".\")\n    }()\n}\nwg.Wait()\nA WaitGroup doesn't know anything about the goroutines it manages. It works with an internal counter. Calling wg.Add(1) increments the counter by one, while wg.Done() decrements it. wg.Wait() blocks the calling goroutine until the counter reaches zero.The Go method combines Add, starting a goroutine, and Done:var wg sync.WaitGroup\nfor range 10 {\n    wg.Go(func() {\n        fmt.Print(\".\")\n    })\n}\nwg.Wait()\nAll methods are safe to use from multiple goroutines.Normally, all Add calls happen before Wait. But technically, there's nothing stopping you from doing some of the Add calls before Wait and some after (from another goroutine).You can call Wait from multiple goroutines. They will all block until the group's counter reaches zero.#\nData racesA data race happens when multiple goroutines access shared data, and at least one of them modifies it. We need to protect the data from this kind of concurrent access.A data race doesn't always cause a runtime panic. That's why Go provides a special tool called the race detector. You can turn it on with the race flag, which works with the test, run, build, and install commands.var total int\n\n// There's a data race on total.\nvar wg sync.WaitGroup\nwg.Go(func() { total++ })\nwg.Go(func() { total++ })\nwg.Wait()\n\nfmt.Println(\"total:\", total)\n==================\nWARNING: DATA RACE\n...\n2\nFound 1 data race(s)\nChannels are safe for concurrent reading and writing, and they don't cause data races.Ways to prevent data races:Avoid concurrent data modification (typically by using channels).Synchronize access with mutexes.Use only atomic operations.Race conditionsA race condition happens when an unpredictable order of operations from multiple goroutines leads to an incorrect system state:// There's a race condition when working with balance.\nwithdraw := func(amount int) {\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nIf individual operations are concurrent-safe, Go's race detector won't find any issues. Because of this, it doesn't catch race conditions:You can't fully eliminate uncertainty in a concurrent environment. Events will happen in an unpredictable order \u2014 that's just how concurrency works. However, you can prevent a race condition \u2014 often by protecting a composite operation with a mutex:var mu sync.Mutex\nwithdraw := func(amount int) {\n    mu.Lock()\n    defer mu.Unlock()\n\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nCompare-and-setSometimes you can prevent a race condition without using mutexes by applying an atomic compare-and-set operation or one of its flavors:// CompareAndSet changes the value to new if the current value equals old.\n// Returns true if the value was changed.\nCompareAndSet(old, new any) bool\n\n// CompareAndSwap changes the value to new if the current value equals old.\n// Returns the old value.\nCompareAndSwap(old, new any) any\n\n// CompareAndDelete deletes the value if the current value equals old.\n// Returns true if the value was deleted.\nCompareAndDelete(old any) bool\n\n// etc\nThe idea is always the same:Check if the assumed (old) state matches reality.If it does, change the state to new.If not, do nothing.#\nMutexesThe sync.Mutex type protects shared data and parts of your code from being accessed concurrently:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        mu.Lock()\n        time.Sleep(time.Millisecond)\n        total++\n        mu.Unlock()\n    })\n}\nwg.Wait()\nThe mutex guarantees that only one goroutine can run the code between Lock() and Unlock() at a time.A mutex is used in these situations:When multiple goroutines are modifying the same data.When one goroutine is modifying the data and others are reading it.If all goroutines are only reading the data, you don't need a mutex.TryLockThe TryLock method tries to lock the mutex, just like a regular Lock. But if it can't, it returns false right away instead of blocking the goroutine:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        if !mu.TryLock() {\n            return\n        }\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\nwg.Wait()\nRWMutexThe sync.RWMutex type distinguishes between readers and writers. It provides two sets of methods:Lock / Unlock lock and unlock the mutex for both reading and writing.RLock / RUnlock lock and unlock the mutex for reading only.var total int\nvar mu sync.RWMutex\nvar wg sync.WaitGroup\n\n// 10 writers.\nfor range 10 {\n    wg.Go(func() {\n        mu.Lock()\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\n\n// 10 readers.\nfor range 10 {\n    wg.Go(func() {\n        // Try switching from RLock/RUnlock to Lock/Unlock\n        //and see how it affects the elapsed time.\n        mu.RLock()\n        defer mu.RUnlock()\n        time.Sleep(time.Millisecond)\n        _ = total\n    })\n}\n\nwg.Wait()\nHere's how it works:If a goroutine locks the mutex with Lock(), other goroutines will be blocked if they try to use Lock() or RLock().If a goroutine locks the mutex with RLock(), other goroutines can also lock it with RLock() without being blocked.If at least one goroutine has locked the mutex with RLock(), other goroutines will be blocked if they try to use Lock().This creates a \"single writer, multiple readers\" setup.LockerBoth sync.Mutex and sync.RWMutex implement the same sync.Locker interface:type Locker interface {\n    Lock()\n    Unlock()\n}\nBy using Locker instead of a specific mutex type, you can build components that don't depend on a specific lock implementation. This lets the client decide which lock to use.Channel as mutexYou can use a channel instead of a mutex to protect shared data:var total int\nlock := make(chan struct{}, 1)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Wait()\n#\nSemaphoresA semaphore is like a container with N available slots and two operations: acquire to take a slot and release to free a slot. Here are the semaphore rules:Calling acquire takes a free slot.If there are no free slots, acquire blocks the goroutine that called it.Calling release frees up a previously taken slot.If there are any goroutines blocked on acquire when release is called, one of them will immediately take the freed slot and unblock.You can implement a simple semaphore with a buffered channel, where N is the channel's size. To acquire the semaphore, send a value into the channel. To release it, take a value from the channel:// Try changing nConc and see how the elapsed time changes.\nconst nConc = 4\nconst nCalls = 100\nsema := make(chan struct{}, nConc)\n\nvar wg sync.WaitGroup\nfor range nCalls {\n    sema <- struct{}{} // acquire\n    wg.Go(func() {\n        defer func() { <-sema }() // release\n        time.Sleep(time.Millisecond) // do some work\n    })\n}\nwg.Wait()\nFor more complex situations, use the golang.org/x/sync/semaphore package.RendezvousA rendezvous lets two goroutines wait for each other:There are two goroutines \u2014 G1 and G2 \u2014 and each one can signal that it's ready.If G1 signals but G2 hasn't yet, G1 blocks and waits.If G2 signals but G1 hasn't yet, G2 blocks and waits.When both have signaled, they both unblock and continue running.You can implement a simple rendezvous with a wait group:var rend sync.WaitGroup\nrend.Add(2)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Wait()\nbefore rendezvous\nbefore rendezvous\nafter rendezvous\nafter rendezvous\nBarrierA barrier is a general case of a rendezvous. It lets N goroutines wait for each other:The barrier has a counter (starting at 0) and a threshold N.Each goroutine that reaches the barrier increases the counter by 1.The barrier blocks any goroutine that reaches it.Once the counter reaches N, the barrier unblocks all waiting goroutines.You can implement a simple barrier with a wait group:const n = 4\nvar bar sync.WaitGroup\nbar.Add(n)\n\nvar wg sync.WaitGroup\nfor range n {\n    wg.Go(func() {\n        fmt.Println(\"before the barrier\")\n        bar.Done()\n        bar.Wait()\n        fmt.Println(\"after the barrier\")\n    })\n}\nwg.Wait()\nbefore the barrier\nbefore the barrier\nbefore the barrier\nbefore the barrier\nafter the barrier\nafter the barrier\nafter the barrier\nafter the barrier\n#\nSignalingThe sync.Cond (conditional variable) type lets one goroutine signal to another that it's ready, and lets the other goroutine wait for that signal.A Cond includes a mutex and has two methods \u2014 Wait and Signal.Wait unlocks the mutex and suspends the goroutine until it receives a signal.Signal wakes the goroutine that is waiting on Wait.When Wait wakes up, it locks the mutex again.cond := sync.NewCond(&sync.Mutex{})\ndone := false\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    cond.L.Lock()\n    fmt.Println(\"G1 is ready to signal\")\n    done = true\n    cond.Signal()\n    cond.L.Unlock()\n})\nwg.Go(func() {\n    cond.L.Lock()\n    for !done {\n        cond.Wait()\n    }\n    fmt.Println(\"G2 received the signal\")\n    cond.L.Unlock()\n})\nwg.Wait()\nG1 is ready to signal\nG2 received the signal\nIf there are multiple waiting goroutines when Signal is called, only one of them will be resumed. If there are no waiting goroutines, Signal does nothing.You can also use the Broadcast method. While Signal wakes up only one goroutine waiting on Cond.Wait, the Broadcast method wakes up all such goroutines.You can signal with a channel:signal := make(chan struct{}, 1)\n\ngo func() {\n    // do something\n    signal <- struct{}{}\n}()\n\ngo func() {\n    <-signal\n    // do something\n}()\nAnd broadcast too:broadcast := make(chan struct{})\n\ngo func() {\n    // do something\n    close(broadcast)\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\nBroadcasting with a condition variable is limited: it only sends a signal, not the actual data, and it only works once. With channels, you can build a publish/subscribe system that doesn't have these limitations:type Publisher struct {\n    sbox []chan int // subscription channels\n    mu   sync.Mutex // protects the state\n}\n\nfunc (p *Publisher) Subscribe() <-chan int {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    sub := make(chan int, 1)\n    p.sbox = append(p.sbox, sub)\n    return sub\n}\n\nfunc (p *Publisher) Broadcast(v int) {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    for _, sub := range p.sbox {\n        select {\n        case sub <- v:\n        default:\n        }\n    }\n}\n#\nRun onceThe sync.Once type makes sure that the given function runs only once. If multiple goroutines call Once.Do at the same time, only one will run the function, while the others will wait until it returns:total := 0\ninitState := func() {\n    total += 1\n}\n\nvar once sync.Once\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Wait()\nOnce is perfect for one-time initialization or cleanup in a concurrent environment.Besides the Once type, the sync package also includes three convenience once-functions:// Calls f only once.\nfunc (o *Once) Do(f func())\n\n// Returns a function that calls f only once.\nfunc OnceFunc(f func()) func()\n\n// Returns a function that calls f only once\n// and returns the value from that first call.\nfunc OnceValue[T any](f func() T) func() T\n\n// Returns a function that calls f only once\n// and returns the pair of values from that first call.\nfunc OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)\n#\nObject poolThe sync.Pool type helps reuse memory instead of allocating it every time, which reduces the load on the garbage collector:pool := sync.Pool{\n    New: func() any {\n        buf := make([]byte, 1024)\n        return &buf\n    },\n}\n\n// Only allocates 4*1024 B, despite 4000 loop iterations.\nvar wg sync.WaitGroup\nfor range 4 {\n    wg.Go(func() {\n        for range 1000 {\n            buf := pool.Get().(*[]byte)\n            sink = buf\n            pool.Put(buf)\n        }\n    })\n}\nwg.Wait()\nGet takes an item from the pool. If there are no available items, it creates a new one using New (which we have to define ourselves, since the pool doesn't know anything about the items it creates). Put returns an item back to the pool.Things to keep in mind:New should return a pointer, not a value, to reduce memory copying and avoid extra allocations.The pool has no size limit. If you start 1000 more goroutines that all call Get at the same time, 1000 more buffers will be allocated.After an item is returned to the pool with Put, you shouldn't use it anymore (since another goroutine might already have taken and started using it).#\nAtomicsAn operation without synchronization can only be truly atomic if it translates to a single processor instruction. Such operations don't need locks and won't cause issues when called concurrently (even the write operations).There are only a few atomics, and they're all found in the sync/atomic package:Int32     Bool\nInt64     Value\nUint32    Pointer\nUint64\nEach atomic type provides the following methods:Load reads the value of a variable.Store sets a new value.Swap sets a new value (like Store) and returns the old one.CompareAndSwap sets a new value only if the current value is still what you expect it to be.var n atomic.Int32\nn.Store(10)\nswapped := n.CompareAndSwap(10, 42)\nfmt.Println(\"CompareAndSwap 10 -> 42:\", swapped)\nfmt.Println(\"n =\", n.Load())\nCompareAndSwap 10 -> 42: true\nn = 42\nNumeric types also provide an Add method that increments the value by the specified amount.All methods are either translated into a single CPU instruction or are otherwise guaranteed to be atomic, so they are safe to use from multiple goroutines.The composition of atomics is always non-atomic:var delta atomic.Int32\nvar counter atomic.Int32\n\nfunc increment() {\n    // Not atomic; causes a race condition.\n    delta.Add(1)\n    sleep(10)\n    counter.Add(delta.Load())\n}\n\n// After 100 concurrent increments,\n// the final value is NOT guaranteed.\nA bulletproof way to make a composite operation atomic and prevent race conditions is to use a mutex:var delta int32\nvar counter int32\nvar mu sync.Mutex\n\nfunc increment() {\n    // Atomic; doesn't cause a race condition.\n    mu.Lock()\n    delta += 1\n    sleep(10)\n    counter += delta\n    mu.Unlock()\n}\n\n// After 100 concurrent increments, the final value is guaranteed:\n// counter = 1+2+...+100 = 5050\nSometimes you can use an atomic type instead of a mutex to exit early:type Gate struct {\n    closed atomic.Bool\n}\n\nfunc (g *Gate) Close() {\n    if !g.closed.CompareAndSwap(false, true) {\n        return // ignore repeated calls\n    }\n    // The gate is closed.\n    // We can free resources now.\n}\n#\nTestingIf your concurrent program uses channels or custom types with synchronization methods like Wait, you can use those in your tests. This way, your tests won't be much more complicated than if the code were synchronous:// Calc calculates something asynchronously.\nfunc Calc() <-chan int {\n    out := make(chan int, 1)\n    go func() {\n        out <- 42\n    }()\n    return out\n}\nfunc Test(t *testing.T) {\n    // Wait for the Calc goroutine to finish.\n    got := <-Calc()\n    if got != 42 {\n        t.Errorf(\"got: %v; want: 42\", got)\n    }\n}\nIf there aren't any suitable synchronization \"handles\" in the code you're testing, you can use the synctest package. It exports two functions:func Test(t *testing.T, f func(*testing.T))\nfunc Wait()\nsynctest.Test runs an isolated bubble. The bubble uses a fake clock, and you can manually control goroutine synchronization with synctest.Wait.synctest.Wait blocks until all goroutines in the bubble \u2014 except the one that called Wait \u2014 have either finished or are durably blocked. This lets you wait for a specific goroutine to finish or get blocked, so you can check the program's state:// NewProc starts the calculation.\nfunc NewProc() *Proc {\n    p := &Proc{done: make(chan struct{})}\n    go func() {\n        p.res = 42\n        <-p.done // (X)\n        p.res = 0\n    }()\n    return p\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        p := NewProc()\n        defer p.Stop()\n\n        // Wait for the goroutine to block at point X.\n        synctest.Wait()\n        if got := p.Res(); got != 42 {\n            t.Fatalf(\"got %v, want 42\", got)\n        }\n    })\n}\nThe fake clock in synctest.Test move forward only if: \u278a all goroutines in the bubble are durably blocked; \u278b there's a future moment when at least one goroutine will unblock; and \u278c synctest.Wait isn't running. Thanks to this, time-dependent tests run instantly:// Calc processes a value from the input channel.\n// Times out if no input is received after 3 seconds.\nfunc Calc(in chan int) (int, error) {\n    select {\n    case v := <-in:\n        return v * 2, nil\n    case <-time.After(3 * time.Second):\n        return 0, ErrTimeout\n    }\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ch := make(chan int)\n        got, err := Calc(ch) // runs instantly\n\n        if err != ErrTimeout {\n            t.Errorf(\"got: %v; want: %v\", err, ErrTimeout)\n        }\n        if got != 0 {\n            t.Errorf(\"got: %v; want: 0\", got)\n        }\n    })\n}\nThe following operations durably block a goroutine:A blocking send or receive on a channel created within the bubble.A blocking select statement where every case is a channel created within the bubble.Calling Cond.Wait.Calling WaitGroup.Wait if all WaitGroup.Add calls were made inside the bubble.Calling time.Sleep.Blocking on mutexes, I/O, or system calls is not considered durable, and the synctest bubble can't handle them.#\nSchedulingAt the hardware level, CPU cores are responsible for running parallel tasks.At the operating system level, a thread is the basic unit of execution. There are usually many more threads than CPU cores, so the operating system's scheduler decides which threads to run and which ones to pause.At the Go runtime level, a goroutine is the basic unit of execution. The runtime scheduler runs a fixed number of OS threads, often one per CPU core. There can be many more goroutines than threads, so the scheduler decides which goroutines to run on the available threads and which ones to pause. The scheduler keeps switching between goroutines to make sure each one gets a turn to run on a thread, instead of waiting in line forever.  CPU                  OS                   Go runtime\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Cores    \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Threads  \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Goroutines \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThis is how Go handles concurrency.Goroutine schedulerThe goroutine scheduler's job is to run M goroutines on N operating system threads, where M can be much larger than N. Here's a very simplified version of it's algorithm:If there's a free thread, assign it a goroutine from the queue.If a running goroutine gets blocked (for example, while reading from a channel), put it back in the queue and assign a different goroutine to the thread.If a running goroutine gets stuck in a syscall, start a new thread to run other goroutines until the blocked goroutine finishes the syscall.Check the running goroutines every 10 ms. Preempt long-running goroutines and return them to the queue to prevent starvation.\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G17 \u2502\u2502 G18 \u2502\u2502 G19 \u2502\u2502 G20 \u2502                        queue\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G15 \u2502      \u2502 G16 \u2502      \u2502 G13 \u2502      \u2502 G14 \u2502      running\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502            \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread E \u2502 \u2502 Thread F \u2502 \u2502 Thread C \u2502 \u2502 Thread D \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G11 \u2502      \u2502 G12 \u2502                                syscalls\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread A \u2502 \u2502 Thread B \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThe number of threads running Go code is controlled by the GOMAXPROCS environment variable or the runtime.GOMAXPROCS function.A goroutine is a structure that starts out using about 2 KB of memory, mostly for its stack. The stack can grow if needed. Since goroutines are so lightweight, you can run tens of thousands or even hundreds of thousands of them on a small machine.#\nDiagnosticsTo troubleshoot concurrent programs in production, we use metrics, profiling, and tracing.Metrics show how the Go runtime is performing, like how much heap memory it uses or how long garbage collection pauses take. Each metric has a unique name and a value, which can be a number or a histogram.You can use the runtime/metrics package to get a complete list of metrics or check the values of specific ones:samples := []metrics.Sample{\n    {Name: \"/sched/gomaxprocs:threads\"},\n    {Name: \"/sched/goroutines:goroutines\"},\n}\nmetrics.Read(samples)\n\nfor _, s := range samples {\n    fmt.Printf(\"%s: %v\\n\", s.Name, s.Value.Uint64())\n}\n/sched/gomaxprocs:threads: 8\n/sched/goroutines:goroutines: 1\nIn practice, people rarely do this manually. Instead, all metrics are automatically exported using Prometheus or OpenTelemetry libraries.Profiling helps you understand exactly what the program is doing, what resources it uses, and where in the code this happens. Go uses a sampling profiler that's suitable for production.The most commonly used profiles are CPU, which shows how much processor time each function uses, and heap, which shows how much heap memory each function uses. Goroutine, block, and mutex profiles help identify problems related to concurrency.The easiest way to add a profiler to your app is by using the net/http/pprof package. To collect a profile with the given name, call the /debug/pprof/{name} endpoint. To view the collected profile, use the go tool pprof utility:go tool pprof -proto \\\n  \"http://localhost:6060/debug/pprof/profile?seconds=N\" > cpu.pprof\ngo tool pprof -http=localhost:8080 cpu.pprof\nYou can also profile manually:// CPU profile.\nfile, _ := os.Create(\"cpu.prof\")\ndefer file.Close()\npprof.StartCPUProfile(file)\ndefer pprof.StopCPUProfile()\n// ...\n// Any other profile.\nfile, _ := os.Create(name + \".prof\")\ndefer file.Close()\npprof.Lookup(name).WriteTo(file, 0)\nTracing records certain types of events while the program is running, mainly those related to concurrency and memory. When the profiling server from the net/http/pprof package is running, call the /debug/pprof/trace endpoint to collect a trace. To view the results, use the go tool trace utility.You can also collect a trace manually:file, _ := os.Create(\"trace.out\")\ndefer file.Close()\ntrace.Start(file)\ndefer trace.Stop()\n// ...\nYou can set up automatic tracing with a sliding window that's limited by size or duration. This is called \"flight recording\". It lets you always keep a recent trace available in case something goes wrong:cfg := trace.FlightRecorderConfig{\n    MinAge:   5 * time.Second,\n    MaxBytes: 3 << 20, // 3MB\n}\nrec := trace.NewFlightRecorder(cfg)\nrec.Start()\ndefer rec.Stop()\n#\nFinal thoughtsWe've covered a number of Go tools for writing concurrent programs:Goroutines for running concurrent tasks.Channels and select as flexible communication tools.Timers and tickers for working with time.Context for canceling operations.Wait groups for synchronizing goroutines.Mutexes to prevent race conditions.Condition variables for signaling events.Once for safe one-time initialization.Pools to reduce garbage collector load.Atomic operations.If you like the book, please recommend it to your friends or colleagues. If you're interested, check out my other books and projects.I'm glad you finished the book. Thank you, and I'll see you next time!\u2605\u00a0Subscribe to keep up with new posts."
                ],
                "output": "readability",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:12.252308+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmpbl0mdj2g",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-09-27T10:09:55.048576+00:00",
                "index_texts": [
                    "Go concurrency distilledThis mini-book provides a brief overview of many concurrency topics in Go. Each topic comes with interactive examples \u2014 feel free to experiment with them by changing the code and clicking Run. There's also a PDF version with static examples.This is a quick refresher on Go concurrency, not a beginner's guide. If you want to learn concurrency from the ground up with practical exercises, check out my other book \u2014 Gist of Go: Concurrency.The book is AI-free.Goroutines\u00a0\u2022\nChannels\u00a0\u2022\nSelect\u00a0\u2022\nPipelines\u00a0\u2022\nTime\u00a0\u2022\nContext\u00a0\u2022\nWait groups\u00a0\u2022\nData races\u00a0\u2022\nRace conditions\u00a0\u2022\nMutexes\u00a0\u2022\nSemaphores\u00a0\u2022\nSignaling\u00a0\u2022\nRun once\u00a0\u2022\nObject pool\u00a0\u2022\nAtomics\u00a0\u2022\nTesting\u00a0\u2022\nScheduling\u00a0\u2022\nDiagnostics\u00a0\u2022\nFinal thoughts#\nGoroutinesThe foundation of concurrency in Go is goroutines \u2013 functions started with the go keyword:func main() {\n    var wg sync.WaitGroup\n    wg.Add(2)\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 1\")\n    }()\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 2\")\n    }()\n    wg.Wait()\n}\nThe Go runtime juggles these goroutines and distributes them among operating system threads running on CPU cores. Compared to OS threads, goroutines are lightweight, so you can create hundreds or thousands of them.Goroutines are completely independent. The main function is also a goroutine, but it starts implicitly when the program starts. When main ends, other goroutines also shut down.We use a wait group (sync.WaitGroup) to wait for goroutines to finish in the example above. A wait group has a counter inside. Calling Add(n) increments it by n, while Done() decrements it by one. Wait() blocks the calling goroutine (in this case, main) until the counter reaches zero. This way, main waits for both workers to finish before it exits.WaitGroup.Go automatically increments the wait group counter, runs a function in a goroutine, and decrements the counter when it's done:func main() {\n    var wg sync.WaitGroup\n    wg.Go(func() {\n        fmt.Println(\"worker 1\")\n    })\n    wg.Go(func() {\n        fmt.Println(\"worker 2\")\n    })\n    wg.Wait()\n}\n#\nChannelsGoroutines can pass values to each other through channels. A channel is like a window where one goroutine can throw something and another can catch it:func main() {\n    messages := make(chan string)\n\n    go func() { messages <- \"ping\" }()\n\n    msg := <-messages\n    fmt.Println(msg)\n}\nSending a value through a channel is a synchronous operation. When the sending goroutine writes a value to the channel (ch <- val), it blocks and waits for someone to receive that value (<-ch). Only then does it continue.Output channelReturning an output channel from a function and filling it within an internal goroutine is a common pattern in Go. This allows the caller to receive values through the channel while the owning function retains control of it:func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nClosing a channelTo signal readers that all data has been sent, the writer goroutine closes the channel with close():func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nThe reader checks the channel's status with a second value (\"comma OK\") when reading:func main() {\n    in := generate(5, 10)\n    for {\n        num, ok := <-in\n        if !ok {\n            break\n        }\n        fmt.Print(num, \" \")\n    }\n}\nWhile the channel is open, the reader receives the next value and a true status. If the channel is closed, the reader gets a zero value and a false status.A channel can only be closed once. Closing it again or writing to a closed channel causes a panic.The only reason to close a channel is to signal to its readers that all data has been sent. If this isn't important to the readers, then you don't need to close it. When a channel is no longer used, Go's garbage collector will free its resources, whether it's closed or not.Channel iterationrange automatically reads the next value from the channel and checks if it's closed. If the channel is closed, it exits the loop:func main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nRange over a channel returns a single value, not a pair, unlike range over a slice.Directional channelsYou can protect yourself from accidental write/close errors by setting the channel direction. Channels can be:chan (bidirectional): for reading and writing (default);chan<- (send-only): for writing only;<-chan (receive-only): for reading only.You can't read from a send-only channel or write to a receive-only channel (nor can you close it).Channels are usually initialized for both reading and writing, and specified as directional in function parameters. Go automatically converts a regular channel to a directional one:stream := make(chan int)\n\ngo func(in chan<- int) {\n    in <- 42\n}(stream)\n\nfunc(out <-chan int) {\n    fmt.Println(<-out)\n}(stream)\nBuffered channelsBuffered channels work like a FIFO queue with a fixed-size buffer for storing values.As long as the buffer has free space, writing to the channel doesn't block the goroutine. Similarly, as long as the buffer contains values, reading from the channel doesn't block the goroutine:stream := make(chan int, 3)\nstream <- 11\nstream <- 12\nstream <- 13\n\nfmt.Println(<-stream)\nfmt.Println(<-stream)\nBy default, if you don't specify a buffer size, a channel is unbuffered (buffer size equals zero).Buffered channels work with the built-in len() and cap() functions:stream := make(chan int, 3)\nstream <- 11\nfmt.Println(cap(stream), len(stream))\nReading from a closed buffered channel returns values from the buffer and a true status. Once all values are taken, it returns a zero value and a false status, like a regular channel:stream := make(chan int, 1)\nstream <- 11\nclose(stream)\n\nval, ok := <-stream\nfmt.Println(val, ok)\n// 11 true\n\nval, ok = <-stream\nfmt.Println(val, ok)\n// 0 false\nnil channelLike any type in Go, channels have a zero value, which is nil.Writing to or reading from a nil channel blocks the goroutine indefinitely:var stream chan int\n\ngo func() {\n    // blocks forever\n    stream <- 1\n}()\n\n// blocks forever\n<-stream\nClosing a nil channel causes a panic:var stream chan int\nclose(stream)\n// panic: close of nil channel\n#\nSelectThe select statement is somewhat like switch, but specifically designed for channels. Here's what it does:Checks which cases are not blocked.If multiple cases are ready, randomly selects one to execute.If all cases are blocked and there is a default case, executes it.If all cases are blocked and there is no default case, waits until one is ready.Select is used to manage data flow in pipelines:// merge sends values from in1 and in2 to the output channel.\nfunc merge(in1, in2 <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for in1 != nil || in2 != nil {\n            select {\n            case val1, ok := <-in1:\n                if ok { out <- val1 } else { in1 = nil }\n            case val2, ok := <-in2:\n                if ok { out <- val2 } else { in2 = nil }\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send 10..12 to in1, 20..22 to in2,\n// and call merge(in1, in2)\nTo cancel goroutines:// process modifies values from in and send them to out\n// until in is exhausted or cancel is closed.\nfunc process(cancel chan struct{}, in <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        for val := range in {\n            select {\n            case out <- val*10:\n            case <-cancel:\n                fmt.Println(\"canceled\")\n                return\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send values 11 and 12 to in\n// and then call close(cancel)\nFor non-blocking operations:// multiplier returns a function that multiplies\n// the input by 10 and sends it to the channel\n// or returns an error if the channel is busy.\nfunc multiplier(ch chan<- int) func(n int) error {\n    return func(n int) error {\n        select {\n        case ch <- n*10:\n            return nil\n        default:\n            return errors.New(\"busy\")\n        }\n    }\n}\n\nfunc main() {\n    nums := make(chan int, 1)\n    multiply := multiplier(nums)\n\n    err := multiply(11)\n    fmt.Println(<-nums, err)\n    // 110 <nil>\n\n    err = multiply(12)\n    fmt.Println(<-nums, err)\n    // 120 <nil>\n\n    err = multiply(13)\n    err = multiply(14)\n    fmt.Println(err)\n    // busy\n}\nAnd for much more.#\nPipelinesA pipeline is a sequence of operations where each step takes input data, processes it in a specific way, and outputs it. The input and output of each operation is a channel.A typical pipeline looks like this:Reader: Reads input data from a file, database, or network.N processors: Transform, filter, aggregate, or enrich data using external sources.Writer: Writes the processed data to a file, database, or network.func read[T any]() <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for {\n            // read data from somewere\n            data := // ...\n            out <- data\n        }\n    }()\n    return out\n}\n\nfunc process[T any](in <-chan T) <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for inData := range in {\n            // process the data\n            outData = // ...\n            out <- outData\n        }\n    }()\n    return out\n}\n\nfunc write[T any](in <-chan T) <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        for data := range in {\n            // write the data\n        }\n    }()\n    return done\n}\nOutput channelA goroutine can signal other goroutines that it has finished its work using an output channel:func generate(start, stop int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nDone channelIf a goroutine doesn't need to return results, it can signal completion using a done channel:func work() <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fmt.Println(\"work done\")\n    }()\n    return done\n}\n\nfunc main() {\n    done := work()\n    <-done\n}\nCancel channelTo terminate a goroutine early, a calling goroutine can use a cancel channel:func generate(cancel chan struct{}, n int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := 1; i <= n; i++ {\n            select {\n            case out <- i:\n            case <-cancel:\n                return\n            }\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    cancel := make(chan struct{})\n    defer close(cancel)\n\n    nums := generate(cancel, 10)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n}\nError handlingThere are three approaches to error handling in concurrent pipelines.\u278a Return on the first error:// calculate produces answers for the given numbers.\nfunc process(in <-chan int) (<-chan int, <-chan error) {\n\tout := make(chan Answer)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err != nil {\n\t\t\t\terrc <- err  // return with error\n\t\t\t\treturn\n\t\t\t}\n\t\t\tout <- ans\n\t\t}\n\t\terrc <- nil          // return with nil\n\t}()\n\treturn out, errc\n}\n\u278b Use a result type:// Result contains an answer or an error.\ntype Result struct {\n\tanswer int\n\terr    error\n}\n\n// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int) <-chan Result {\n\tout := make(chan Result)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tout <- Result{ans, err}  // return answer + error\n\t\t}\n\t}()\n\treturn out\n}\n\u278c Collect errors separately:// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int, errc chan<- error) <-chan int {\n\tout := make(chan Answer)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err == nil {\n\t\t\t\tout <- ans   // send answer\n\t\t\t} else {\n\t\t\t\terrc <- err  // or error\n\t\t\t}\n\t\t}\n\t}()\n\treturn out\n}\n#\nTimeBesides handling date and time, the time package offers tools for managing time-sensitive operations in concurrent programs.Aftertime.After() returns a channel that is initially empty, but receives a value after the timeout period. It's useful for timing out operations:// withTimeout executes a function with a given timeout.\nfunc withTimeout(timeout time.Duration, fn func()) error {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fn()\n    }()\n\n    // blocks until fn completes or the timer expires,\n    // whichever happens first\n    select {\n    case <-done:\n        return nil\n    case <-time.After(timeout):\n        return errors.New(\"timeout\")\n    }\n}\nwithTimeout() waits for fn() to complete, but thanks to time.After(), it won't wait longer than the timeout duration:func main() {\n    var err error\n\n    // completes in time\n    err = withTimeout(\n        50*time.Millisecond,\n        func() { fmt.Println(\"work done\") },\n    )\n    fmt.Println(\"err =\", err)\n\n    // gets canceled on timeout\n    err = withTimeout(\n        50*time.Millisecond,\n        func() {\n            time.Sleep(100 * time.Millisecond)\n            fmt.Println(\"work done\")\n        },\n    )\n    fmt.Println(\"err =\", err)\n}\nwork done\nerr = <nil>\nerr = timeout\nTimerA timer (time.Timer) is a structure with a C channel to which it sends the current time when it triggers (expires). Timers are useful for planning future executions:done := make(chan struct{})\n\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C  // blocks for 50ms\n    fmt.Println(\"work done at\", eventTime)\n    close(done)\n}()\n\n<-done\nwork done at 2009-11-10 23:00:00.05\nStop() stops the timer and returns true if it hasn't expired yet, and false otherwise:// timer expires after 50ms\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C\n    fmt.Println(\"work done at\", eventTime)\n}()\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n} else {\n    fmt.Println(\"too late to cancel\")\n}\nIt's often more convenient to use the time.AfterFunc() wrapper function. It waits for duration d and then executes function f:done := make(chan struct{})\nwork := func() {\n    fmt.Println(\"work done\")\n    close(done)\n}\n\n// executes work after 50ms\ntime.AfterFunc(50*time.Millisecond, work)\n<-done\ntime.AfterFunc() returns a timer that you can cancel before execution starts:// executes the function after 50ms\ntimer := time.AfterFunc(50*time.Millisecond, func() {})\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n}\nIf a timer is used in a loop, it's better to create a single timer and reset it instead of creating a new instance on each iteration:// consumer reads tokens from the input channel and alerts\n// if a value does not appear in a channel after an hour.\nfunc consumer(in <-chan token) {\n    const timeout = time.Hour\n    timer := time.NewTimer(timeout)\n    for {\n        timer.Reset(timeout)\n        select {\n        case <-in:\n            // do stuff\n        case <-timer.C:\n            // log warning\n        }\n    }\n}\n\n// Suppose we send 10,000 values to the in channel\n// and measure memory usage.\nMemory used: 4 KB, # allocations: 6\nTickerA ticker is like a timer, but it keeps firing until you stop it. Tickers are useful for executing periodic tasks:// fires every 50ms\nticker := time.NewTicker(50 * time.Millisecond)\ndefer ticker.Stop()\n\ngo func() {\n    for {\n        // waits for ticker to fire on each iteration\n        at := <-ticker.C\n        fmt.Println(\"work done at\", at)\n    }\n}()\n\n// enough time for the ticker to fire 3 times\ntime.Sleep(160*time.Millisecond)\nticker.Stop()\nwork done at 2009-11-10 23:00:00.05\nwork done at 2009-11-10 23:00:00.10\nwork done at 2009-11-10 23:00:00.15\nNewTicker(d) creates a ticker that sends the current time to the channel C at interval d. You must stop the ticker eventually with Stop() to free up resources.If the channel reader can't keep up with the ticker, the ticker will skip ticks.#\nContextThe main purpose of context is to cancel operations, either manually or by timeout/deadline.The function accepts a context and uses its Done() channel to listen for cancellation:// work performs a task for 50 ms unless canceled.\n// Returns an error when canceled.\nfunc work(ctx context.Context) error {\n    done := make(chan struct{})\n\n    go func() {\n        time.Sleep(50 * time.Millisecond)\n        fmt.Println(\"work done\")\n        close(done)\n    }()\n\n    select {\n    case <-done:\n        return nil\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}\nCancel manually (context.Canceled error):func main() {\n    // empty context\n    ctx := context.Background()\n    // manual canellation context\n    ctx, cancel := context.WithCancel(ctx)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    // cancels after 10 ms\n    time.Sleep(10 * time.Millisecond)\n    cancel()\n    <-done\n}\nCancel by timeout (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels after 10 ms\n    ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nCancel by deadline (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels at now + 10 ms\n    deadline := time.Now().Add(10 * time.Millisecond)\n    ctx, cancel := context.WithDeadline(ctx, deadline)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nContext is layered. A context object is immutable. To add new properties to a context, a new (child) context is created based on the old (parent) context. The shorter timeout between the parent and child contexts always wins. The child context can only shorten the parent's timeout, not extend it:func main() {\n    // parent context with a 100 ms timeout\n    const dur100ms = 100 * time.Millisecond\n    parentCtx, cancel := context.WithTimeout(context.Background(), dur100ms)\n    defer cancel()\n\n    // child context with a 10 ms timeout\n    const dur10ms = 10 * time.Millisecond\n    childCtx, cancel := context.WithTimeout(parentCtx, dur10ms)\n    defer cancel()\n\n    // now the work gets canceled\n    err := work(childCtx)\n    fmt.Println(\"err =\", err)\n}\nerr = context deadline exceeded\nMultiple cancels are safe. You can call cancel() on the context as many times as you want. The first cancel will work, and the rest will be ignored.You can specify a custom cancellation cause using context.WithCancelCause(), context.WithTimeoutCause() and context.WithDeadlineCause(). This cause is accessible through context.Cause():ctx, cancel := context.WithCancelCause(context.Background())\ncancel(errors.New(\"the night is dark\"))\nfmt.Println(context.Cause(ctx))\nYou can register a function to execute when the context is canceled with context.AfterFunc():ctx, cancel := context.WithCancel(context.Background())\ncleanup := func() { fmt.Println(\"cleanup\") }\ncontext.AfterFunc(ctx, cleanup)\ncancel()\ntime.Sleep(10 * time.Millisecond)\nContext can pass additional information about a call using context.WithValue(), which creates a context with a value for a specific key. But it's generally better to avoid passing values in context. It's better to use explicit parameters or custom structs instead.#\nWait groupsThe sync.WaitGroup type lets you wait for one or more goroutines to finish:const n = 10\nvar wg sync.WaitGroup\nwg.Add(n)\nfor range n {\n    go func() {\n        defer wg.Done()\n        fmt.Print(\".\")\n    }()\n}\nwg.Wait()\nA WaitGroup doesn't know anything about the goroutines it manages. It works with an internal counter. Calling wg.Add(1) increments the counter by one, while wg.Done() decrements it. wg.Wait() blocks the calling goroutine until the counter reaches zero.The Go method combines Add, starting a goroutine, and Done:var wg sync.WaitGroup\nfor range 10 {\n    wg.Go(func() {\n        fmt.Print(\".\")\n    })\n}\nwg.Wait()\nAll methods are safe to use from multiple goroutines.Normally, all Add calls happen before Wait. But technically, there's nothing stopping you from doing some of the Add calls before Wait and some after (from another goroutine).You can call Wait from multiple goroutines. They will all block until the group's counter reaches zero.#\nData racesA data race happens when multiple goroutines access shared data, and at least one of them modifies it. We need to protect the data from this kind of concurrent access.A data race doesn't always cause a runtime panic. That's why Go provides a special tool called the race detector. You can turn it on with the race flag, which works with the test, run, build, and install commands.var total int\n\n// There's a data race on total.\nvar wg sync.WaitGroup\nwg.Go(func() { total++ })\nwg.Go(func() { total++ })\nwg.Wait()\n\nfmt.Println(\"total:\", total)\n==================\nWARNING: DATA RACE\n...\n2\nFound 1 data race(s)\nChannels are safe for concurrent reading and writing, and they don't cause data races.Ways to prevent data races:Avoid concurrent data modification (typically by using channels).Synchronize access with mutexes.Use only atomic operations.Race conditionsA race condition happens when an unpredictable order of operations from multiple goroutines leads to an incorrect system state:// There's a race condition when working with balance.\nwithdraw := func(amount int) {\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nIf individual operations are concurrent-safe, Go's race detector won't find any issues. Because of this, it doesn't catch race conditions:You can't fully eliminate uncertainty in a concurrent environment. Events will happen in an unpredictable order \u2014 that's just how concurrency works. However, you can prevent a race condition \u2014 often by protecting a composite operation with a mutex:var mu sync.Mutex\nwithdraw := func(amount int) {\n    mu.Lock()\n    defer mu.Unlock()\n\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nCompare-and-setSometimes you can prevent a race condition without using mutexes by applying an atomic compare-and-set operation or one of its flavors:// CompareAndSet changes the value to new if the current value equals old.\n// Returns true if the value was changed.\nCompareAndSet(old, new any) bool\n\n// CompareAndSwap changes the value to new if the current value equals old.\n// Returns the old value.\nCompareAndSwap(old, new any) any\n\n// CompareAndDelete deletes the value if the current value equals old.\n// Returns true if the value was deleted.\nCompareAndDelete(old any) bool\n\n// etc\nThe idea is always the same:Check if the assumed (old) state matches reality.If it does, change the state to new.If not, do nothing.#\nMutexesThe sync.Mutex type protects shared data and parts of your code from being accessed concurrently:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        mu.Lock()\n        time.Sleep(time.Millisecond)\n        total++\n        mu.Unlock()\n    })\n}\nwg.Wait()\nThe mutex guarantees that only one goroutine can run the code between Lock() and Unlock() at a time.A mutex is used in these situations:When multiple goroutines are modifying the same data.When one goroutine is modifying the data and others are reading it.If all goroutines are only reading the data, you don't need a mutex.TryLockThe TryLock method tries to lock the mutex, just like a regular Lock. But if it can't, it returns false right away instead of blocking the goroutine:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        if !mu.TryLock() {\n            return\n        }\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\nwg.Wait()\nRWMutexThe sync.RWMutex type distinguishes between readers and writers. It provides two sets of methods:Lock / Unlock lock and unlock the mutex for both reading and writing.RLock / RUnlock lock and unlock the mutex for reading only.var total int\nvar mu sync.RWMutex\nvar wg sync.WaitGroup\n\n// 10 writers.\nfor range 10 {\n    wg.Go(func() {\n        mu.Lock()\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\n\n// 10 readers.\nfor range 10 {\n    wg.Go(func() {\n        // Try switching from RLock/RUnlock to Lock/Unlock\n        //and see how it affects the elapsed time.\n        mu.RLock()\n        defer mu.RUnlock()\n        time.Sleep(time.Millisecond)\n        _ = total\n    })\n}\n\nwg.Wait()\nHere's how it works:If a goroutine locks the mutex with Lock(), other goroutines will be blocked if they try to use Lock() or RLock().If a goroutine locks the mutex with RLock(), other goroutines can also lock it with RLock() without being blocked.If at least one goroutine has locked the mutex with RLock(), other goroutines will be blocked if they try to use Lock().This creates a \"single writer, multiple readers\" setup.LockerBoth sync.Mutex and sync.RWMutex implement the same sync.Locker interface:type Locker interface {\n    Lock()\n    Unlock()\n}\nBy using Locker instead of a specific mutex type, you can build components that don't depend on a specific lock implementation. This lets the client decide which lock to use.Channel as mutexYou can use a channel instead of a mutex to protect shared data:var total int\nlock := make(chan struct{}, 1)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Wait()\n#\nSemaphoresA semaphore is like a container with N available slots and two operations: acquire to take a slot and release to free a slot. Here are the semaphore rules:Calling acquire takes a free slot.If there are no free slots, acquire blocks the goroutine that called it.Calling release frees up a previously taken slot.If there are any goroutines blocked on acquire when release is called, one of them will immediately take the freed slot and unblock.You can implement a simple semaphore with a buffered channel, where N is the channel's size. To acquire the semaphore, send a value into the channel. To release it, take a value from the channel:// Try changing nConc and see how the elapsed time changes.\nconst nConc = 4\nconst nCalls = 100\nsema := make(chan struct{}, nConc)\n\nvar wg sync.WaitGroup\nfor range nCalls {\n    sema <- struct{}{} // acquire\n    wg.Go(func() {\n        defer func() { <-sema }() // release\n        time.Sleep(time.Millisecond) // do some work\n    })\n}\nwg.Wait()\nFor more complex situations, use the golang.org/x/sync/semaphore package.RendezvousA rendezvous lets two goroutines wait for each other:There are two goroutines \u2014 G1 and G2 \u2014 and each one can signal that it's ready.If G1 signals but G2 hasn't yet, G1 blocks and waits.If G2 signals but G1 hasn't yet, G2 blocks and waits.When both have signaled, they both unblock and continue running.You can implement a simple rendezvous with a wait group:var rend sync.WaitGroup\nrend.Add(2)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Wait()\nbefore rendezvous\nbefore rendezvous\nafter rendezvous\nafter rendezvous\nBarrierA barrier is a general case of a rendezvous. It lets N goroutines wait for each other:The barrier has a counter (starting at 0) and a threshold N.Each goroutine that reaches the barrier increases the counter by 1.The barrier blocks any goroutine that reaches it.Once the counter reaches N, the barrier unblocks all waiting goroutines.You can implement a simple barrier with a wait group:const n = 4\nvar bar sync.WaitGroup\nbar.Add(n)\n\nvar wg sync.WaitGroup\nfor range n {\n    wg.Go(func() {\n        fmt.Println(\"before the barrier\")\n        bar.Done()\n        bar.Wait()\n        fmt.Println(\"after the barrier\")\n    })\n}\nwg.Wait()\nbefore the barrier\nbefore the barrier\nbefore the barrier\nbefore the barrier\nafter the barrier\nafter the barrier\nafter the barrier\nafter the barrier\n#\nSignalingThe sync.Cond (conditional variable) type lets one goroutine signal to another that it's ready, and lets the other goroutine wait for that signal.A Cond includes a mutex and has two methods \u2014 Wait and Signal.Wait unlocks the mutex and suspends the goroutine until it receives a signal.Signal wakes the goroutine that is waiting on Wait.When Wait wakes up, it locks the mutex again.cond := sync.NewCond(&sync.Mutex{})\ndone := false\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    cond.L.Lock()\n    fmt.Println(\"G1 is ready to signal\")\n    done = true\n    cond.Signal()\n    cond.L.Unlock()\n})\nwg.Go(func() {\n    cond.L.Lock()\n    for !done {\n        cond.Wait()\n    }\n    fmt.Println(\"G2 received the signal\")\n    cond.L.Unlock()\n})\nwg.Wait()\nG1 is ready to signal\nG2 received the signal\nIf there are multiple waiting goroutines when Signal is called, only one of them will be resumed. If there are no waiting goroutines, Signal does nothing.You can also use the Broadcast method. While Signal wakes up only one goroutine waiting on Cond.Wait, the Broadcast method wakes up all such goroutines.You can signal with a channel:signal := make(chan struct{}, 1)\n\ngo func() {\n    // do something\n    signal <- struct{}{}\n}()\n\ngo func() {\n    <-signal\n    // do something\n}()\nAnd broadcast too:broadcast := make(chan struct{})\n\ngo func() {\n    // do something\n    close(broadcast)\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\nBroadcasting with a condition variable is limited: it only sends a signal, not the actual data, and it only works once. With channels, you can build a publish/subscribe system that doesn't have these limitations:type Publisher struct {\n    sbox []chan int // subscription channels\n    mu   sync.Mutex // protects the state\n}\n\nfunc (p *Publisher) Subscribe() <-chan int {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    sub := make(chan int, 1)\n    p.sbox = append(p.sbox, sub)\n    return sub\n}\n\nfunc (p *Publisher) Broadcast(v int) {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    for _, sub := range p.sbox {\n        select {\n        case sub <- v:\n        default:\n        }\n    }\n}\n#\nRun onceThe sync.Once type makes sure that the given function runs only once. If multiple goroutines call Once.Do at the same time, only one will run the function, while the others will wait until it returns:total := 0\ninitState := func() {\n    total += 1\n}\n\nvar once sync.Once\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Wait()\nOnce is perfect for one-time initialization or cleanup in a concurrent environment.Besides the Once type, the sync package also includes three convenience once-functions:// Calls f only once.\nfunc (o *Once) Do(f func())\n\n// Returns a function that calls f only once.\nfunc OnceFunc(f func()) func()\n\n// Returns a function that calls f only once\n// and returns the value from that first call.\nfunc OnceValue[T any](f func() T) func() T\n\n// Returns a function that calls f only once\n// and returns the pair of values from that first call.\nfunc OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)\n#\nObject poolThe sync.Pool type helps reuse memory instead of allocating it every time, which reduces the load on the garbage collector:pool := sync.Pool{\n    New: func() any {\n        buf := make([]byte, 1024)\n        return &buf\n    },\n}\n\n// Only allocates 4*1024 B, despite 4000 loop iterations.\nvar wg sync.WaitGroup\nfor range 4 {\n    wg.Go(func() {\n        for range 1000 {\n            buf := pool.Get().(*[]byte)\n            sink = buf\n            pool.Put(buf)\n        }\n    })\n}\nwg.Wait()\nGet takes an item from the pool. If there are no available items, it creates a new one using New (which we have to define ourselves, since the pool doesn't know anything about the items it creates). Put returns an item back to the pool.Things to keep in mind:New should return a pointer, not a value, to reduce memory copying and avoid extra allocations.The pool has no size limit. If you start 1000 more goroutines that all call Get at the same time, 1000 more buffers will be allocated.After an item is returned to the pool with Put, you shouldn't use it anymore (since another goroutine might already have taken and started using it).#\nAtomicsAn operation without synchronization can only be truly atomic if it translates to a single processor instruction. Such operations don't need locks and won't cause issues when called concurrently (even the write operations).There are only a few atomics, and they're all found in the sync/atomic package:Int32     Bool\nInt64     Value\nUint32    Pointer\nUint64\nEach atomic type provides the following methods:Load reads the value of a variable.Store sets a new value.Swap sets a new value (like Store) and returns the old one.CompareAndSwap sets a new value only if the current value is still what you expect it to be.var n atomic.Int32\nn.Store(10)\nswapped := n.CompareAndSwap(10, 42)\nfmt.Println(\"CompareAndSwap 10 -> 42:\", swapped)\nfmt.Println(\"n =\", n.Load())\nCompareAndSwap 10 -> 42: true\nn = 42\nNumeric types also provide an Add method that increments the value by the specified amount.All methods are either translated into a single CPU instruction or are otherwise guaranteed to be atomic, so they are safe to use from multiple goroutines.The composition of atomics is always non-atomic:var delta atomic.Int32\nvar counter atomic.Int32\n\nfunc increment() {\n    // Not atomic; causes a race condition.\n    delta.Add(1)\n    sleep(10)\n    counter.Add(delta.Load())\n}\n\n// After 100 concurrent increments,\n// the final value is NOT guaranteed.\nA bulletproof way to make a composite operation atomic and prevent race conditions is to use a mutex:var delta int32\nvar counter int32\nvar mu sync.Mutex\n\nfunc increment() {\n    // Atomic; doesn't cause a race condition.\n    mu.Lock()\n    delta += 1\n    sleep(10)\n    counter += delta\n    mu.Unlock()\n}\n\n// After 100 concurrent increments, the final value is guaranteed:\n// counter = 1+2+...+100 = 5050\nSometimes you can use an atomic type instead of a mutex to exit early:type Gate struct {\n    closed atomic.Bool\n}\n\nfunc (g *Gate) Close() {\n    if !g.closed.CompareAndSwap(false, true) {\n        return // ignore repeated calls\n    }\n    // The gate is closed.\n    // We can free resources now.\n}\n#\nTestingIf your concurrent program uses channels or custom types with synchronization methods like Wait, you can use those in your tests. This way, your tests won't be much more complicated than if the code were synchronous:// Calc calculates something asynchronously.\nfunc Calc() <-chan int {\n    out := make(chan int, 1)\n    go func() {\n        out <- 42\n    }()\n    return out\n}\nfunc Test(t *testing.T) {\n    // Wait for the Calc goroutine to finish.\n    got := <-Calc()\n    if got != 42 {\n        t.Errorf(\"got: %v; want: 42\", got)\n    }\n}\nIf there aren't any suitable synchronization \"handles\" in the code you're testing, you can use the synctest package. It exports two functions:func Test(t *testing.T, f func(*testing.T))\nfunc Wait()\nsynctest.Test runs an isolated bubble. The bubble uses a fake clock, and you can manually control goroutine synchronization with synctest.Wait.synctest.Wait blocks until all goroutines in the bubble \u2014 except the one that called Wait \u2014 have either finished or are durably blocked. This lets you wait for a specific goroutine to finish or get blocked, so you can check the program's state:// NewProc starts the calculation.\nfunc NewProc() *Proc {\n    p := &Proc{done: make(chan struct{})}\n    go func() {\n        p.res = 42\n        <-p.done // (X)\n        p.res = 0\n    }()\n    return p\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        p := NewProc()\n        defer p.Stop()\n\n        // Wait for the goroutine to block at point X.\n        synctest.Wait()\n        if got := p.Res(); got != 42 {\n            t.Fatalf(\"got %v, want 42\", got)\n        }\n    })\n}\nThe fake clock in synctest.Test move forward only if: \u278a all goroutines in the bubble are durably blocked; \u278b there's a future moment when at least one goroutine will unblock; and \u278c synctest.Wait isn't running. Thanks to this, time-dependent tests run instantly:// Calc processes a value from the input channel.\n// Times out if no input is received after 3 seconds.\nfunc Calc(in chan int) (int, error) {\n    select {\n    case v := <-in:\n        return v * 2, nil\n    case <-time.After(3 * time.Second):\n        return 0, ErrTimeout\n    }\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ch := make(chan int)\n        got, err := Calc(ch) // runs instantly\n\n        if err != ErrTimeout {\n            t.Errorf(\"got: %v; want: %v\", err, ErrTimeout)\n        }\n        if got != 0 {\n            t.Errorf(\"got: %v; want: 0\", got)\n        }\n    })\n}\nThe following operations durably block a goroutine:A blocking send or receive on a channel created within the bubble.A blocking select statement where every case is a channel created within the bubble.Calling Cond.Wait.Calling WaitGroup.Wait if all WaitGroup.Add calls were made inside the bubble.Calling time.Sleep.Blocking on mutexes, I/O, or system calls is not considered durable, and the synctest bubble can't handle them.#\nSchedulingAt the hardware level, CPU cores are responsible for running parallel tasks.At the operating system level, a thread is the basic unit of execution. There are usually many more threads than CPU cores, so the operating system's scheduler decides which threads to run and which ones to pause.At the Go runtime level, a goroutine is the basic unit of execution. The runtime scheduler runs a fixed number of OS threads, often one per CPU core. There can be many more goroutines than threads, so the scheduler decides which goroutines to run on the available threads and which ones to pause. The scheduler keeps switching between goroutines to make sure each one gets a turn to run on a thread, instead of waiting in line forever.  CPU                  OS                   Go runtime\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Cores    \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Threads  \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Goroutines \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThis is how Go handles concurrency.Goroutine schedulerThe goroutine scheduler's job is to run M goroutines on N operating system threads, where M can be much larger than N. Here's a very simplified version of it's algorithm:If there's a free thread, assign it a goroutine from the queue.If a running goroutine gets blocked (for example, while reading from a channel), put it back in the queue and assign a different goroutine to the thread.If a running goroutine gets stuck in a syscall, start a new thread to run other goroutines until the blocked goroutine finishes the syscall.Check the running goroutines every 10 ms. Preempt long-running goroutines and return them to the queue to prevent starvation.\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G17 \u2502\u2502 G18 \u2502\u2502 G19 \u2502\u2502 G20 \u2502                        queue\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G15 \u2502      \u2502 G16 \u2502      \u2502 G13 \u2502      \u2502 G14 \u2502      running\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502            \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread E \u2502 \u2502 Thread F \u2502 \u2502 Thread C \u2502 \u2502 Thread D \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G11 \u2502      \u2502 G12 \u2502                                syscalls\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread A \u2502 \u2502 Thread B \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThe number of threads running Go code is controlled by the GOMAXPROCS environment variable or the runtime.GOMAXPROCS function.A goroutine is a structure that starts out using about 2 KB of memory, mostly for its stack. The stack can grow if needed. Since goroutines are so lightweight, you can run tens of thousands or even hundreds of thousands of them on a small machine.#\nDiagnosticsTo troubleshoot concurrent programs in production, we use metrics, profiling, and tracing.Metrics show how the Go runtime is performing, like how much heap memory it uses or how long garbage collection pauses take. Each metric has a unique name and a value, which can be a number or a histogram.You can use the runtime/metrics package to get a complete list of metrics or check the values of specific ones:samples := []metrics.Sample{\n    {Name: \"/sched/gomaxprocs:threads\"},\n    {Name: \"/sched/goroutines:goroutines\"},\n}\nmetrics.Read(samples)\n\nfor _, s := range samples {\n    fmt.Printf(\"%s: %v\\n\", s.Name, s.Value.Uint64())\n}\n/sched/gomaxprocs:threads: 8\n/sched/goroutines:goroutines: 1\nIn practice, people rarely do this manually. Instead, all metrics are automatically exported using Prometheus or OpenTelemetry libraries.Profiling helps you understand exactly what the program is doing, what resources it uses, and where in the code this happens. Go uses a sampling profiler that's suitable for production.The most commonly used profiles are CPU, which shows how much processor time each function uses, and heap, which shows how much heap memory each function uses. Goroutine, block, and mutex profiles help identify problems related to concurrency.The easiest way to add a profiler to your app is by using the net/http/pprof package. To collect a profile with the given name, call the /debug/pprof/{name} endpoint. To view the collected profile, use the go tool pprof utility:go tool pprof -proto \\\n  \"http://localhost:6060/debug/pprof/profile?seconds=N\" > cpu.pprof\ngo tool pprof -http=localhost:8080 cpu.pprof\nYou can also profile manually:// CPU profile.\nfile, _ := os.Create(\"cpu.prof\")\ndefer file.Close()\npprof.StartCPUProfile(file)\ndefer pprof.StopCPUProfile()\n// ...\n// Any other profile.\nfile, _ := os.Create(name + \".prof\")\ndefer file.Close()\npprof.Lookup(name).WriteTo(file, 0)\nTracing records certain types of events while the program is running, mainly those related to concurrency and memory. When the profiling server from the net/http/pprof package is running, call the /debug/pprof/trace endpoint to collect a trace. To view the results, use the go tool trace utility.You can also collect a trace manually:file, _ := os.Create(\"trace.out\")\ndefer file.Close()\ntrace.Start(file)\ndefer trace.Stop()\n// ...\nYou can set up automatic tracing with a sliding window that's limited by size or duration. This is called \"flight recording\". It lets you always keep a recent trace available in case something goes wrong:cfg := trace.FlightRecorderConfig{\n    MinAge:   5 * time.Second,\n    MaxBytes: 3 << 20, // 3MB\n}\nrec := trace.NewFlightRecorder(cfg)\nrec.Start()\ndefer rec.Stop()\n#\nFinal thoughtsWe've covered a number of Go tools for writing concurrent programs:Goroutines for running concurrent tasks.Channels and select as flexible communication tools.Timers and tickers for working with time.Context for canceling operations.Wait groups for synchronizing goroutines.Mutexes to prevent race conditions.Condition variables for signaling events.Once for safe one-time initialization.Pools to reduce garbage collector load.Atomic operations.If you like the book, please recommend it to your friends or colleagues. If you're interested, check out my other books and projects.I'm glad you finished the book. Thank you, and I'll see you next time!\u2605\u00a0Subscribe to keep up with new posts."
                ],
                "output": "readability",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:49.557285+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "/app/node_modules/readability-extractor/readability-extractor",
                    "/tmp/tmp6p364bpn",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "0.0.11",
                "end_ts": "2026-09-27T16:27:35.702357+00:00",
                "index_texts": [
                    "Go concurrency distilledThis mini-book provides a brief overview of many concurrency topics in Go. Each topic comes with interactive examples \u2014 feel free to experiment with them by changing the code and clicking Run. There's also a PDF version with static examples.This is a quick refresher on Go concurrency, not a beginner's guide. If you want to learn concurrency from the ground up with practical exercises, check out my other book \u2014 Gist of Go: Concurrency.The book is AI-free.Goroutines\u00a0\u2022\nChannels\u00a0\u2022\nSelect\u00a0\u2022\nPipelines\u00a0\u2022\nTime\u00a0\u2022\nContext\u00a0\u2022\nWait groups\u00a0\u2022\nData races\u00a0\u2022\nRace conditions\u00a0\u2022\nMutexes\u00a0\u2022\nSemaphores\u00a0\u2022\nSignaling\u00a0\u2022\nRun once\u00a0\u2022\nObject pool\u00a0\u2022\nAtomics\u00a0\u2022\nTesting\u00a0\u2022\nScheduling\u00a0\u2022\nDiagnostics\u00a0\u2022\nFinal thoughts#\nGoroutinesThe foundation of concurrency in Go is goroutines \u2013 functions started with the go keyword:func main() {\n    var wg sync.WaitGroup\n    wg.Add(2)\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 1\")\n    }()\n    go func() {\n        defer wg.Done()\n        fmt.Println(\"worker 2\")\n    }()\n    wg.Wait()\n}\nThe Go runtime juggles these goroutines and distributes them among operating system threads running on CPU cores. Compared to OS threads, goroutines are lightweight, so you can create hundreds or thousands of them.Goroutines are completely independent. The main function is also a goroutine, but it starts implicitly when the program starts. When main ends, other goroutines also shut down.We use a wait group (sync.WaitGroup) to wait for goroutines to finish in the example above. A wait group has a counter inside. Calling Add(n) increments it by n, while Done() decrements it by one. Wait() blocks the calling goroutine (in this case, main) until the counter reaches zero. This way, main waits for both workers to finish before it exits.WaitGroup.Go automatically increments the wait group counter, runs a function in a goroutine, and decrements the counter when it's done:func main() {\n    var wg sync.WaitGroup\n    wg.Go(func() {\n        fmt.Println(\"worker 1\")\n    })\n    wg.Go(func() {\n        fmt.Println(\"worker 2\")\n    })\n    wg.Wait()\n}\n#\nChannelsGoroutines can pass values to each other through channels. A channel is like a window where one goroutine can throw something and another can catch it:func main() {\n    messages := make(chan string)\n\n    go func() { messages <- \"ping\" }()\n\n    msg := <-messages\n    fmt.Println(msg)\n}\nSending a value through a channel is a synchronous operation. When the sending goroutine writes a value to the channel (ch <- val), it blocks and waits for someone to receive that value (<-ch). Only then does it continue.Output channelReturning an output channel from a function and filling it within an internal goroutine is a common pattern in Go. This allows the caller to receive values through the channel while the owning function retains control of it:func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nClosing a channelTo signal readers that all data has been sent, the writer goroutine closes the channel with close():func generate(start, stop int) chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\nThe reader checks the channel's status with a second value (\"comma OK\") when reading:func main() {\n    in := generate(5, 10)\n    for {\n        num, ok := <-in\n        if !ok {\n            break\n        }\n        fmt.Print(num, \" \")\n    }\n}\nWhile the channel is open, the reader receives the next value and a true status. If the channel is closed, the reader gets a zero value and a false status.A channel can only be closed once. Closing it again or writing to a closed channel causes a panic.The only reason to close a channel is to signal to its readers that all data has been sent. If this isn't important to the readers, then you don't need to close it. When a channel is no longer used, Go's garbage collector will free its resources, whether it's closed or not.Channel iterationrange automatically reads the next value from the channel and checks if it's closed. If the channel is closed, it exits the loop:func main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nRange over a channel returns a single value, not a pair, unlike range over a slice.Directional channelsYou can protect yourself from accidental write/close errors by setting the channel direction. Channels can be:chan (bidirectional): for reading and writing (default);chan<- (send-only): for writing only;<-chan (receive-only): for reading only.You can't read from a send-only channel or write to a receive-only channel (nor can you close it).Channels are usually initialized for both reading and writing, and specified as directional in function parameters. Go automatically converts a regular channel to a directional one:stream := make(chan int)\n\ngo func(in chan<- int) {\n    in <- 42\n}(stream)\n\nfunc(out <-chan int) {\n    fmt.Println(<-out)\n}(stream)\nBuffered channelsBuffered channels work like a FIFO queue with a fixed-size buffer for storing values.As long as the buffer has free space, writing to the channel doesn't block the goroutine. Similarly, as long as the buffer contains values, reading from the channel doesn't block the goroutine:stream := make(chan int, 3)\nstream <- 11\nstream <- 12\nstream <- 13\n\nfmt.Println(<-stream)\nfmt.Println(<-stream)\nBy default, if you don't specify a buffer size, a channel is unbuffered (buffer size equals zero).Buffered channels work with the built-in len() and cap() functions:stream := make(chan int, 3)\nstream <- 11\nfmt.Println(cap(stream), len(stream))\nReading from a closed buffered channel returns values from the buffer and a true status. Once all values are taken, it returns a zero value and a false status, like a regular channel:stream := make(chan int, 1)\nstream <- 11\nclose(stream)\n\nval, ok := <-stream\nfmt.Println(val, ok)\n// 11 true\n\nval, ok = <-stream\nfmt.Println(val, ok)\n// 0 false\nnil channelLike any type in Go, channels have a zero value, which is nil.Writing to or reading from a nil channel blocks the goroutine indefinitely:var stream chan int\n\ngo func() {\n    // blocks forever\n    stream <- 1\n}()\n\n// blocks forever\n<-stream\nClosing a nil channel causes a panic:var stream chan int\nclose(stream)\n// panic: close of nil channel\n#\nSelectThe select statement is somewhat like switch, but specifically designed for channels. Here's what it does:Checks which cases are not blocked.If multiple cases are ready, randomly selects one to execute.If all cases are blocked and there is a default case, executes it.If all cases are blocked and there is no default case, waits until one is ready.Select is used to manage data flow in pipelines:// merge sends values from in1 and in2 to the output channel.\nfunc merge(in1, in2 <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for in1 != nil || in2 != nil {\n            select {\n            case val1, ok := <-in1:\n                if ok { out <- val1 } else { in1 = nil }\n            case val2, ok := <-in2:\n                if ok { out <- val2 } else { in2 = nil }\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send 10..12 to in1, 20..22 to in2,\n// and call merge(in1, in2)\nTo cancel goroutines:// process modifies values from in and send them to out\n// until in is exhausted or cancel is closed.\nfunc process(cancel chan struct{}, in <-chan int) <-chan int {\n    out := make(chan int)\n    go func() {\n        for val := range in {\n            select {\n            case out <- val*10:\n            case <-cancel:\n                fmt.Println(\"canceled\")\n                return\n            }\n        }\n    }()\n    return out\n}\n\n// Suppose we send values 11 and 12 to in\n// and then call close(cancel)\nFor non-blocking operations:// multiplier returns a function that multiplies\n// the input by 10 and sends it to the channel\n// or returns an error if the channel is busy.\nfunc multiplier(ch chan<- int) func(n int) error {\n    return func(n int) error {\n        select {\n        case ch <- n*10:\n            return nil\n        default:\n            return errors.New(\"busy\")\n        }\n    }\n}\n\nfunc main() {\n    nums := make(chan int, 1)\n    multiply := multiplier(nums)\n\n    err := multiply(11)\n    fmt.Println(<-nums, err)\n    // 110 <nil>\n\n    err = multiply(12)\n    fmt.Println(<-nums, err)\n    // 120 <nil>\n\n    err = multiply(13)\n    err = multiply(14)\n    fmt.Println(err)\n    // busy\n}\nAnd for much more.#\nPipelinesA pipeline is a sequence of operations where each step takes input data, processes it in a specific way, and outputs it. The input and output of each operation is a channel.A typical pipeline looks like this:Reader: Reads input data from a file, database, or network.N processors: Transform, filter, aggregate, or enrich data using external sources.Writer: Writes the processed data to a file, database, or network.func read[T any]() <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for {\n            // read data from somewere\n            data := // ...\n            out <- data\n        }\n    }()\n    return out\n}\n\nfunc process[T any](in <-chan T) <-chan T {\n    out := make(chan T)\n    go func() {\n        defer close(out)\n        for inData := range in {\n            // process the data\n            outData = // ...\n            out <- outData\n        }\n    }()\n    return out\n}\n\nfunc write[T any](in <-chan T) <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        for data := range in {\n            // write the data\n        }\n    }()\n    return done\n}\nOutput channelA goroutine can signal other goroutines that it has finished its work using an output channel:func generate(start, stop int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := start; i < stop; i++ {\n            out <- i\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    nums := generate(5, 10)\n    for n := range nums {\n        fmt.Print(n, \" \")\n    }\n}\nDone channelIf a goroutine doesn't need to return results, it can signal completion using a done channel:func work() <-chan struct{} {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fmt.Println(\"work done\")\n    }()\n    return done\n}\n\nfunc main() {\n    done := work()\n    <-done\n}\nCancel channelTo terminate a goroutine early, a calling goroutine can use a cancel channel:func generate(cancel chan struct{}, n int) <-chan int {\n    out := make(chan int)\n    go func() {\n        defer close(out)\n        for i := 1; i <= n; i++ {\n            select {\n            case out <- i:\n            case <-cancel:\n                return\n            }\n        }\n    }()\n    return out\n}\n\nfunc main() {\n    cancel := make(chan struct{})\n    defer close(cancel)\n\n    nums := generate(cancel, 10)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n    fmt.Println(<-nums)\n}\nError handlingThere are three approaches to error handling in concurrent pipelines.\u278a Return on the first error:// calculate produces answers for the given numbers.\nfunc process(in <-chan int) (<-chan int, <-chan error) {\n\tout := make(chan Answer)\n\terrc := make(chan error, 1)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err != nil {\n\t\t\t\terrc <- err  // return with error\n\t\t\t\treturn\n\t\t\t}\n\t\t\tout <- ans\n\t\t}\n\t\terrc <- nil          // return with nil\n\t}()\n\treturn out, errc\n}\n\u278b Use a result type:// Result contains an answer or an error.\ntype Result struct {\n\tanswer int\n\terr    error\n}\n\n// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int) <-chan Result {\n\tout := make(chan Result)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tout <- Result{ans, err}  // return answer + error\n\t\t}\n\t}()\n\treturn out\n}\n\u278c Collect errors separately:// calculate produces answers for the given numbers.\nfunc calculate(in <-chan int, errc chan<- error) <-chan int {\n\tout := make(chan Answer)\n\tgo func() {\n\t\tdefer close(out)\n\t\tfor n := range in {\n\t\t\tans, err := fetchAnswer(n)\n\t\t\tif err == nil {\n\t\t\t\tout <- ans   // send answer\n\t\t\t} else {\n\t\t\t\terrc <- err  // or error\n\t\t\t}\n\t\t}\n\t}()\n\treturn out\n}\n#\nTimeBesides handling date and time, the time package offers tools for managing time-sensitive operations in concurrent programs.Aftertime.After() returns a channel that is initially empty, but receives a value after the timeout period. It's useful for timing out operations:// withTimeout executes a function with a given timeout.\nfunc withTimeout(timeout time.Duration, fn func()) error {\n    done := make(chan struct{})\n    go func() {\n        defer close(done)\n        fn()\n    }()\n\n    // blocks until fn completes or the timer expires,\n    // whichever happens first\n    select {\n    case <-done:\n        return nil\n    case <-time.After(timeout):\n        return errors.New(\"timeout\")\n    }\n}\nwithTimeout() waits for fn() to complete, but thanks to time.After(), it won't wait longer than the timeout duration:func main() {\n    var err error\n\n    // completes in time\n    err = withTimeout(\n        50*time.Millisecond,\n        func() { fmt.Println(\"work done\") },\n    )\n    fmt.Println(\"err =\", err)\n\n    // gets canceled on timeout\n    err = withTimeout(\n        50*time.Millisecond,\n        func() {\n            time.Sleep(100 * time.Millisecond)\n            fmt.Println(\"work done\")\n        },\n    )\n    fmt.Println(\"err =\", err)\n}\nwork done\nerr = <nil>\nerr = timeout\nTimerA timer (time.Timer) is a structure with a C channel to which it sends the current time when it triggers (expires). Timers are useful for planning future executions:done := make(chan struct{})\n\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C  // blocks for 50ms\n    fmt.Println(\"work done at\", eventTime)\n    close(done)\n}()\n\n<-done\nwork done at 2009-11-10 23:00:00.05\nStop() stops the timer and returns true if it hasn't expired yet, and false otherwise:// timer expires after 50ms\ntimer := time.NewTimer(50 * time.Millisecond)\ngo func() {\n    eventTime := <-timer.C\n    fmt.Println(\"work done at\", eventTime)\n}()\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n} else {\n    fmt.Println(\"too late to cancel\")\n}\nIt's often more convenient to use the time.AfterFunc() wrapper function. It waits for duration d and then executes function f:done := make(chan struct{})\nwork := func() {\n    fmt.Println(\"work done\")\n    close(done)\n}\n\n// executes work after 50ms\ntime.AfterFunc(50*time.Millisecond, work)\n<-done\ntime.AfterFunc() returns a timer that you can cancel before execution starts:// executes the function after 50ms\ntimer := time.AfterFunc(50*time.Millisecond, func() {})\n\n// after 10ms, the timer hasn't expired yet\ntime.Sleep(10 * time.Millisecond)\n\nif timer.Stop() {\n    fmt.Println(\"execution canceled\")\n}\nIf a timer is used in a loop, it's better to create a single timer and reset it instead of creating a new instance on each iteration:// consumer reads tokens from the input channel and alerts\n// if a value does not appear in a channel after an hour.\nfunc consumer(in <-chan token) {\n    const timeout = time.Hour\n    timer := time.NewTimer(timeout)\n    for {\n        timer.Reset(timeout)\n        select {\n        case <-in:\n            // do stuff\n        case <-timer.C:\n            // log warning\n        }\n    }\n}\n\n// Suppose we send 10,000 values to the in channel\n// and measure memory usage.\nMemory used: 4 KB, # allocations: 6\nTickerA ticker is like a timer, but it keeps firing until you stop it. Tickers are useful for executing periodic tasks:// fires every 50ms\nticker := time.NewTicker(50 * time.Millisecond)\ndefer ticker.Stop()\n\ngo func() {\n    for {\n        // waits for ticker to fire on each iteration\n        at := <-ticker.C\n        fmt.Println(\"work done at\", at)\n    }\n}()\n\n// enough time for the ticker to fire 3 times\ntime.Sleep(160*time.Millisecond)\nticker.Stop()\nwork done at 2009-11-10 23:00:00.05\nwork done at 2009-11-10 23:00:00.10\nwork done at 2009-11-10 23:00:00.15\nNewTicker(d) creates a ticker that sends the current time to the channel C at interval d. You must stop the ticker eventually with Stop() to free up resources.If the channel reader can't keep up with the ticker, the ticker will skip ticks.#\nContextThe main purpose of context is to cancel operations, either manually or by timeout/deadline.The function accepts a context and uses its Done() channel to listen for cancellation:// work performs a task for 50 ms unless canceled.\n// Returns an error when canceled.\nfunc work(ctx context.Context) error {\n    done := make(chan struct{})\n\n    go func() {\n        time.Sleep(50 * time.Millisecond)\n        fmt.Println(\"work done\")\n        close(done)\n    }()\n\n    select {\n    case <-done:\n        return nil\n    case <-ctx.Done():\n        return ctx.Err()\n    }\n}\nCancel manually (context.Canceled error):func main() {\n    // empty context\n    ctx := context.Background()\n    // manual canellation context\n    ctx, cancel := context.WithCancel(ctx)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    // cancels after 10 ms\n    time.Sleep(10 * time.Millisecond)\n    cancel()\n    <-done\n}\nCancel by timeout (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels after 10 ms\n    ctx, cancel := context.WithTimeout(ctx, 10*time.Millisecond)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nCancel by deadline (context.DeadlineExceeded error):func main() {\n    ctx := context.Background()\n    // cancels at now + 10 ms\n    deadline := time.Now().Add(10 * time.Millisecond)\n    ctx, cancel := context.WithDeadline(ctx, deadline)\n    defer cancel()\n\n    done := make(chan struct{})\n    go func() {\n        // takes 50 ms unless canceled\n        err := work(ctx)\n        fmt.Println(\"err =\", err)\n        close(done)\n    }()\n\n    <-done\n}\nerr = context deadline exceeded\nContext is layered. A context object is immutable. To add new properties to a context, a new (child) context is created based on the old (parent) context. The shorter timeout between the parent and child contexts always wins. The child context can only shorten the parent's timeout, not extend it:func main() {\n    // parent context with a 100 ms timeout\n    const dur100ms = 100 * time.Millisecond\n    parentCtx, cancel := context.WithTimeout(context.Background(), dur100ms)\n    defer cancel()\n\n    // child context with a 10 ms timeout\n    const dur10ms = 10 * time.Millisecond\n    childCtx, cancel := context.WithTimeout(parentCtx, dur10ms)\n    defer cancel()\n\n    // now the work gets canceled\n    err := work(childCtx)\n    fmt.Println(\"err =\", err)\n}\nerr = context deadline exceeded\nMultiple cancels are safe. You can call cancel() on the context as many times as you want. The first cancel will work, and the rest will be ignored.You can specify a custom cancellation cause using context.WithCancelCause(), context.WithTimeoutCause() and context.WithDeadlineCause(). This cause is accessible through context.Cause():ctx, cancel := context.WithCancelCause(context.Background())\ncancel(errors.New(\"the night is dark\"))\nfmt.Println(context.Cause(ctx))\nYou can register a function to execute when the context is canceled with context.AfterFunc():ctx, cancel := context.WithCancel(context.Background())\ncleanup := func() { fmt.Println(\"cleanup\") }\ncontext.AfterFunc(ctx, cleanup)\ncancel()\ntime.Sleep(10 * time.Millisecond)\nContext can pass additional information about a call using context.WithValue(), which creates a context with a value for a specific key. But it's generally better to avoid passing values in context. It's better to use explicit parameters or custom structs instead.#\nWait groupsThe sync.WaitGroup type lets you wait for one or more goroutines to finish:const n = 10\nvar wg sync.WaitGroup\nwg.Add(n)\nfor range n {\n    go func() {\n        defer wg.Done()\n        fmt.Print(\".\")\n    }()\n}\nwg.Wait()\nA WaitGroup doesn't know anything about the goroutines it manages. It works with an internal counter. Calling wg.Add(1) increments the counter by one, while wg.Done() decrements it. wg.Wait() blocks the calling goroutine until the counter reaches zero.The Go method combines Add, starting a goroutine, and Done:var wg sync.WaitGroup\nfor range 10 {\n    wg.Go(func() {\n        fmt.Print(\".\")\n    })\n}\nwg.Wait()\nAll methods are safe to use from multiple goroutines.Normally, all Add calls happen before Wait. But technically, there's nothing stopping you from doing some of the Add calls before Wait and some after (from another goroutine).You can call Wait from multiple goroutines. They will all block until the group's counter reaches zero.#\nData racesA data race happens when multiple goroutines access shared data, and at least one of them modifies it. We need to protect the data from this kind of concurrent access.A data race doesn't always cause a runtime panic. That's why Go provides a special tool called the race detector. You can turn it on with the race flag, which works with the test, run, build, and install commands.var total int\n\n// There's a data race on total.\nvar wg sync.WaitGroup\nwg.Go(func() { total++ })\nwg.Go(func() { total++ })\nwg.Wait()\n\nfmt.Println(\"total:\", total)\n==================\nWARNING: DATA RACE\n...\n2\nFound 1 data race(s)\nChannels are safe for concurrent reading and writing, and they don't cause data races.Ways to prevent data races:Avoid concurrent data modification (typically by using channels).Synchronize access with mutexes.Use only atomic operations.Race conditionsA race condition happens when an unpredictable order of operations from multiple goroutines leads to an incorrect system state:// There's a race condition when working with balance.\nwithdraw := func(amount int) {\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nIf individual operations are concurrent-safe, Go's race detector won't find any issues. Because of this, it doesn't catch race conditions:You can't fully eliminate uncertainty in a concurrent environment. Events will happen in an unpredictable order \u2014 that's just how concurrency works. However, you can prevent a race condition \u2014 often by protecting a composite operation with a mutex:var mu sync.Mutex\nwithdraw := func(amount int) {\n    mu.Lock()\n    defer mu.Unlock()\n\n    if getBalance() < amount {\n        return\n    }\n    time.Sleep(time.Millisecond)\n    setBalance(getBalance() - amount)\n}\n\nsetBalance(50)\n\nvar wg sync.WaitGroup\nwg.Go(func() { withdraw(40) })\nwg.Go(func() { withdraw(40) })\nwg.Wait()\n\nfmt.Println(\"balance:\", getBalance())\nCompare-and-setSometimes you can prevent a race condition without using mutexes by applying an atomic compare-and-set operation or one of its flavors:// CompareAndSet changes the value to new if the current value equals old.\n// Returns true if the value was changed.\nCompareAndSet(old, new any) bool\n\n// CompareAndSwap changes the value to new if the current value equals old.\n// Returns the old value.\nCompareAndSwap(old, new any) any\n\n// CompareAndDelete deletes the value if the current value equals old.\n// Returns true if the value was deleted.\nCompareAndDelete(old any) bool\n\n// etc\nThe idea is always the same:Check if the assumed (old) state matches reality.If it does, change the state to new.If not, do nothing.#\nMutexesThe sync.Mutex type protects shared data and parts of your code from being accessed concurrently:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        mu.Lock()\n        time.Sleep(time.Millisecond)\n        total++\n        mu.Unlock()\n    })\n}\nwg.Wait()\nThe mutex guarantees that only one goroutine can run the code between Lock() and Unlock() at a time.A mutex is used in these situations:When multiple goroutines are modifying the same data.When one goroutine is modifying the data and others are reading it.If all goroutines are only reading the data, you don't need a mutex.TryLockThe TryLock method tries to lock the mutex, just like a regular Lock. But if it can't, it returns false right away instead of blocking the goroutine:var total int\nvar mu sync.Mutex\n\nvar wg sync.WaitGroup\nfor range 100 {\n    wg.Go(func() {\n        if !mu.TryLock() {\n            return\n        }\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\nwg.Wait()\nRWMutexThe sync.RWMutex type distinguishes between readers and writers. It provides two sets of methods:Lock / Unlock lock and unlock the mutex for both reading and writing.RLock / RUnlock lock and unlock the mutex for reading only.var total int\nvar mu sync.RWMutex\nvar wg sync.WaitGroup\n\n// 10 writers.\nfor range 10 {\n    wg.Go(func() {\n        mu.Lock()\n        defer mu.Unlock()\n        time.Sleep(time.Millisecond)\n        total++\n    })\n}\n\n// 10 readers.\nfor range 10 {\n    wg.Go(func() {\n        // Try switching from RLock/RUnlock to Lock/Unlock\n        //and see how it affects the elapsed time.\n        mu.RLock()\n        defer mu.RUnlock()\n        time.Sleep(time.Millisecond)\n        _ = total\n    })\n}\n\nwg.Wait()\nHere's how it works:If a goroutine locks the mutex with Lock(), other goroutines will be blocked if they try to use Lock() or RLock().If a goroutine locks the mutex with RLock(), other goroutines can also lock it with RLock() without being blocked.If at least one goroutine has locked the mutex with RLock(), other goroutines will be blocked if they try to use Lock().This creates a \"single writer, multiple readers\" setup.LockerBoth sync.Mutex and sync.RWMutex implement the same sync.Locker interface:type Locker interface {\n    Lock()\n    Unlock()\n}\nBy using Locker instead of a specific mutex type, you can build components that don't depend on a specific lock implementation. This lets the client decide which lock to use.Channel as mutexYou can use a channel instead of a mutex to protect shared data:var total int\nlock := make(chan struct{}, 1)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Go(func() {\n    lock <- struct{}{}\n    defer func() { <-lock }()\n    total++\n})\nwg.Wait()\n#\nSemaphoresA semaphore is like a container with N available slots and two operations: acquire to take a slot and release to free a slot. Here are the semaphore rules:Calling acquire takes a free slot.If there are no free slots, acquire blocks the goroutine that called it.Calling release frees up a previously taken slot.If there are any goroutines blocked on acquire when release is called, one of them will immediately take the freed slot and unblock.You can implement a simple semaphore with a buffered channel, where N is the channel's size. To acquire the semaphore, send a value into the channel. To release it, take a value from the channel:// Try changing nConc and see how the elapsed time changes.\nconst nConc = 4\nconst nCalls = 100\nsema := make(chan struct{}, nConc)\n\nvar wg sync.WaitGroup\nfor range nCalls {\n    sema <- struct{}{} // acquire\n    wg.Go(func() {\n        defer func() { <-sema }() // release\n        time.Sleep(time.Millisecond) // do some work\n    })\n}\nwg.Wait()\nFor more complex situations, use the golang.org/x/sync/semaphore package.RendezvousA rendezvous lets two goroutines wait for each other:There are two goroutines \u2014 G1 and G2 \u2014 and each one can signal that it's ready.If G1 signals but G2 hasn't yet, G1 blocks and waits.If G2 signals but G1 hasn't yet, G2 blocks and waits.When both have signaled, they both unblock and continue running.You can implement a simple rendezvous with a wait group:var rend sync.WaitGroup\nrend.Add(2)\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Go(func() {\n    fmt.Println(\"before rendezvous\")\n    rend.Done()\n    rend.Wait()\n    fmt.Println(\"after rendezvous\")\n})\nwg.Wait()\nbefore rendezvous\nbefore rendezvous\nafter rendezvous\nafter rendezvous\nBarrierA barrier is a general case of a rendezvous. It lets N goroutines wait for each other:The barrier has a counter (starting at 0) and a threshold N.Each goroutine that reaches the barrier increases the counter by 1.The barrier blocks any goroutine that reaches it.Once the counter reaches N, the barrier unblocks all waiting goroutines.You can implement a simple barrier with a wait group:const n = 4\nvar bar sync.WaitGroup\nbar.Add(n)\n\nvar wg sync.WaitGroup\nfor range n {\n    wg.Go(func() {\n        fmt.Println(\"before the barrier\")\n        bar.Done()\n        bar.Wait()\n        fmt.Println(\"after the barrier\")\n    })\n}\nwg.Wait()\nbefore the barrier\nbefore the barrier\nbefore the barrier\nbefore the barrier\nafter the barrier\nafter the barrier\nafter the barrier\nafter the barrier\n#\nSignalingThe sync.Cond (conditional variable) type lets one goroutine signal to another that it's ready, and lets the other goroutine wait for that signal.A Cond includes a mutex and has two methods \u2014 Wait and Signal.Wait unlocks the mutex and suspends the goroutine until it receives a signal.Signal wakes the goroutine that is waiting on Wait.When Wait wakes up, it locks the mutex again.cond := sync.NewCond(&sync.Mutex{})\ndone := false\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    cond.L.Lock()\n    fmt.Println(\"G1 is ready to signal\")\n    done = true\n    cond.Signal()\n    cond.L.Unlock()\n})\nwg.Go(func() {\n    cond.L.Lock()\n    for !done {\n        cond.Wait()\n    }\n    fmt.Println(\"G2 received the signal\")\n    cond.L.Unlock()\n})\nwg.Wait()\nG1 is ready to signal\nG2 received the signal\nIf there are multiple waiting goroutines when Signal is called, only one of them will be resumed. If there are no waiting goroutines, Signal does nothing.You can also use the Broadcast method. While Signal wakes up only one goroutine waiting on Cond.Wait, the Broadcast method wakes up all such goroutines.You can signal with a channel:signal := make(chan struct{}, 1)\n\ngo func() {\n    // do something\n    signal <- struct{}{}\n}()\n\ngo func() {\n    <-signal\n    // do something\n}()\nAnd broadcast too:broadcast := make(chan struct{})\n\ngo func() {\n    // do something\n    close(broadcast)\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\n\ngo func() {\n    <-broadcast\n    // do something\n}()\nBroadcasting with a condition variable is limited: it only sends a signal, not the actual data, and it only works once. With channels, you can build a publish/subscribe system that doesn't have these limitations:type Publisher struct {\n    sbox []chan int // subscription channels\n    mu   sync.Mutex // protects the state\n}\n\nfunc (p *Publisher) Subscribe() <-chan int {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    sub := make(chan int, 1)\n    p.sbox = append(p.sbox, sub)\n    return sub\n}\n\nfunc (p *Publisher) Broadcast(v int) {\n    p.mu.Lock()\n    defer p.mu.Unlock()\n    for _, sub := range p.sbox {\n        select {\n        case sub <- v:\n        default:\n        }\n    }\n}\n#\nRun onceThe sync.Once type makes sure that the given function runs only once. If multiple goroutines call Once.Do at the same time, only one will run the function, while the others will wait until it returns:total := 0\ninitState := func() {\n    total += 1\n}\n\nvar once sync.Once\n\nvar wg sync.WaitGroup\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Go(func() {\n    once.Do(initState)\n    // do something\n})\nwg.Wait()\nOnce is perfect for one-time initialization or cleanup in a concurrent environment.Besides the Once type, the sync package also includes three convenience once-functions:// Calls f only once.\nfunc (o *Once) Do(f func())\n\n// Returns a function that calls f only once.\nfunc OnceFunc(f func()) func()\n\n// Returns a function that calls f only once\n// and returns the value from that first call.\nfunc OnceValue[T any](f func() T) func() T\n\n// Returns a function that calls f only once\n// and returns the pair of values from that first call.\nfunc OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)\n#\nObject poolThe sync.Pool type helps reuse memory instead of allocating it every time, which reduces the load on the garbage collector:pool := sync.Pool{\n    New: func() any {\n        buf := make([]byte, 1024)\n        return &buf\n    },\n}\n\n// Only allocates 4*1024 B, despite 4000 loop iterations.\nvar wg sync.WaitGroup\nfor range 4 {\n    wg.Go(func() {\n        for range 1000 {\n            buf := pool.Get().(*[]byte)\n            sink = buf\n            pool.Put(buf)\n        }\n    })\n}\nwg.Wait()\nGet takes an item from the pool. If there are no available items, it creates a new one using New (which we have to define ourselves, since the pool doesn't know anything about the items it creates). Put returns an item back to the pool.Things to keep in mind:New should return a pointer, not a value, to reduce memory copying and avoid extra allocations.The pool has no size limit. If you start 1000 more goroutines that all call Get at the same time, 1000 more buffers will be allocated.After an item is returned to the pool with Put, you shouldn't use it anymore (since another goroutine might already have taken and started using it).#\nAtomicsAn operation without synchronization can only be truly atomic if it translates to a single processor instruction. Such operations don't need locks and won't cause issues when called concurrently (even the write operations).There are only a few atomics, and they're all found in the sync/atomic package:Int32     Bool\nInt64     Value\nUint32    Pointer\nUint64\nEach atomic type provides the following methods:Load reads the value of a variable.Store sets a new value.Swap sets a new value (like Store) and returns the old one.CompareAndSwap sets a new value only if the current value is still what you expect it to be.var n atomic.Int32\nn.Store(10)\nswapped := n.CompareAndSwap(10, 42)\nfmt.Println(\"CompareAndSwap 10 -> 42:\", swapped)\nfmt.Println(\"n =\", n.Load())\nCompareAndSwap 10 -> 42: true\nn = 42\nNumeric types also provide an Add method that increments the value by the specified amount.All methods are either translated into a single CPU instruction or are otherwise guaranteed to be atomic, so they are safe to use from multiple goroutines.The composition of atomics is always non-atomic:var delta atomic.Int32\nvar counter atomic.Int32\n\nfunc increment() {\n    // Not atomic; causes a race condition.\n    delta.Add(1)\n    sleep(10)\n    counter.Add(delta.Load())\n}\n\n// After 100 concurrent increments,\n// the final value is NOT guaranteed.\nA bulletproof way to make a composite operation atomic and prevent race conditions is to use a mutex:var delta int32\nvar counter int32\nvar mu sync.Mutex\n\nfunc increment() {\n    // Atomic; doesn't cause a race condition.\n    mu.Lock()\n    delta += 1\n    sleep(10)\n    counter += delta\n    mu.Unlock()\n}\n\n// After 100 concurrent increments, the final value is guaranteed:\n// counter = 1+2+...+100 = 5050\nSometimes you can use an atomic type instead of a mutex to exit early:type Gate struct {\n    closed atomic.Bool\n}\n\nfunc (g *Gate) Close() {\n    if !g.closed.CompareAndSwap(false, true) {\n        return // ignore repeated calls\n    }\n    // The gate is closed.\n    // We can free resources now.\n}\n#\nTestingIf your concurrent program uses channels or custom types with synchronization methods like Wait, you can use those in your tests. This way, your tests won't be much more complicated than if the code were synchronous:// Calc calculates something asynchronously.\nfunc Calc() <-chan int {\n    out := make(chan int, 1)\n    go func() {\n        out <- 42\n    }()\n    return out\n}\nfunc Test(t *testing.T) {\n    // Wait for the Calc goroutine to finish.\n    got := <-Calc()\n    if got != 42 {\n        t.Errorf(\"got: %v; want: 42\", got)\n    }\n}\nIf there aren't any suitable synchronization \"handles\" in the code you're testing, you can use the synctest package. It exports two functions:func Test(t *testing.T, f func(*testing.T))\nfunc Wait()\nsynctest.Test runs an isolated bubble. The bubble uses a fake clock, and you can manually control goroutine synchronization with synctest.Wait.synctest.Wait blocks until all goroutines in the bubble \u2014 except the one that called Wait \u2014 have either finished or are durably blocked. This lets you wait for a specific goroutine to finish or get blocked, so you can check the program's state:// NewProc starts the calculation.\nfunc NewProc() *Proc {\n    p := &Proc{done: make(chan struct{})}\n    go func() {\n        p.res = 42\n        <-p.done // (X)\n        p.res = 0\n    }()\n    return p\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        p := NewProc()\n        defer p.Stop()\n\n        // Wait for the goroutine to block at point X.\n        synctest.Wait()\n        if got := p.Res(); got != 42 {\n            t.Fatalf(\"got %v, want 42\", got)\n        }\n    })\n}\nThe fake clock in synctest.Test move forward only if: \u278a all goroutines in the bubble are durably blocked; \u278b there's a future moment when at least one goroutine will unblock; and \u278c synctest.Wait isn't running. Thanks to this, time-dependent tests run instantly:// Calc processes a value from the input channel.\n// Times out if no input is received after 3 seconds.\nfunc Calc(in chan int) (int, error) {\n    select {\n    case v := <-in:\n        return v * 2, nil\n    case <-time.After(3 * time.Second):\n        return 0, ErrTimeout\n    }\n}\nfunc Test(t *testing.T) {\n    synctest.Test(t, func(t *testing.T) {\n        ch := make(chan int)\n        got, err := Calc(ch) // runs instantly\n\n        if err != ErrTimeout {\n            t.Errorf(\"got: %v; want: %v\", err, ErrTimeout)\n        }\n        if got != 0 {\n            t.Errorf(\"got: %v; want: 0\", got)\n        }\n    })\n}\nThe following operations durably block a goroutine:A blocking send or receive on a channel created within the bubble.A blocking select statement where every case is a channel created within the bubble.Calling Cond.Wait.Calling WaitGroup.Wait if all WaitGroup.Add calls were made inside the bubble.Calling time.Sleep.Blocking on mutexes, I/O, or system calls is not considered durable, and the synctest bubble can't handle them.#\nSchedulingAt the hardware level, CPU cores are responsible for running parallel tasks.At the operating system level, a thread is the basic unit of execution. There are usually many more threads than CPU cores, so the operating system's scheduler decides which threads to run and which ones to pause.At the Go runtime level, a goroutine is the basic unit of execution. The runtime scheduler runs a fixed number of OS threads, often one per CPU core. There can be many more goroutines than threads, so the scheduler decides which goroutines to run on the available threads and which ones to pause. The scheduler keeps switching between goroutines to make sure each one gets a turn to run on a thread, instead of waiting in line forever.  CPU                  OS                   Go runtime\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510  run on \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Cores    \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Threads  \u2502 <\u2500\u2500\u2500\u2500\u2500\u2500 \u2502 Goroutines \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518         \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThis is how Go handles concurrency.Goroutine schedulerThe goroutine scheduler's job is to run M goroutines on N operating system threads, where M can be much larger than N. Here's a very simplified version of it's algorithm:If there's a free thread, assign it a goroutine from the queue.If a running goroutine gets blocked (for example, while reading from a channel), put it back in the queue and assign a different goroutine to the thread.If a running goroutine gets stuck in a syscall, start a new thread to run other goroutines until the blocked goroutine finishes the syscall.Check the running goroutines every 10 ms. Preempt long-running goroutines and return them to the queue to prevent starvation.\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G17 \u2502\u2502 G18 \u2502\u2502 G19 \u2502\u2502 G20 \u2502                        queue\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\u2514\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G15 \u2502      \u2502 G16 \u2502      \u2502 G13 \u2502      \u2502 G14 \u2502      running\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502            \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread E \u2502 \u2502 Thread F \u2502 \u2502 Thread C \u2502 \u2502 Thread D \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n\u250c\u2500\u2500\u2500\u2500\u2500\u2510      \u250c\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 G11 \u2502      \u2502 G12 \u2502                                syscalls\n\u2514\u2500\u2500\u2500\u2500\u2500\u2518      \u2514\u2500\u2500\u2500\u2500\u2500\u2518\n  \u2502            \u2502\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 Thread A \u2502 \u2502 Thread B \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\nThe number of threads running Go code is controlled by the GOMAXPROCS environment variable or the runtime.GOMAXPROCS function.A goroutine is a structure that starts out using about 2 KB of memory, mostly for its stack. The stack can grow if needed. Since goroutines are so lightweight, you can run tens of thousands or even hundreds of thousands of them on a small machine.#\nDiagnosticsTo troubleshoot concurrent programs in production, we use metrics, profiling, and tracing.Metrics show how the Go runtime is performing, like how much heap memory it uses or how long garbage collection pauses take. Each metric has a unique name and a value, which can be a number or a histogram.You can use the runtime/metrics package to get a complete list of metrics or check the values of specific ones:samples := []metrics.Sample{\n    {Name: \"/sched/gomaxprocs:threads\"},\n    {Name: \"/sched/goroutines:goroutines\"},\n}\nmetrics.Read(samples)\n\nfor _, s := range samples {\n    fmt.Printf(\"%s: %v\\n\", s.Name, s.Value.Uint64())\n}\n/sched/gomaxprocs:threads: 8\n/sched/goroutines:goroutines: 1\nIn practice, people rarely do this manually. Instead, all metrics are automatically exported using Prometheus or OpenTelemetry libraries.Profiling helps you understand exactly what the program is doing, what resources it uses, and where in the code this happens. Go uses a sampling profiler that's suitable for production.The most commonly used profiles are CPU, which shows how much processor time each function uses, and heap, which shows how much heap memory each function uses. Goroutine, block, and mutex profiles help identify problems related to concurrency.The easiest way to add a profiler to your app is by using the net/http/pprof package. To collect a profile with the given name, call the /debug/pprof/{name} endpoint. To view the collected profile, use the go tool pprof utility:go tool pprof -proto \\\n  \"http://localhost:6060/debug/pprof/profile?seconds=N\" > cpu.pprof\ngo tool pprof -http=localhost:8080 cpu.pprof\nYou can also profile manually:// CPU profile.\nfile, _ := os.Create(\"cpu.prof\")\ndefer file.Close()\npprof.StartCPUProfile(file)\ndefer pprof.StopCPUProfile()\n// ...\n// Any other profile.\nfile, _ := os.Create(name + \".prof\")\ndefer file.Close()\npprof.Lookup(name).WriteTo(file, 0)\nTracing records certain types of events while the program is running, mainly those related to concurrency and memory. When the profiling server from the net/http/pprof package is running, call the /debug/pprof/trace endpoint to collect a trace. To view the results, use the go tool trace utility.You can also collect a trace manually:file, _ := os.Create(\"trace.out\")\ndefer file.Close()\ntrace.Start(file)\ndefer trace.Stop()\n// ...\nYou can set up automatic tracing with a sliding window that's limited by size or duration. This is called \"flight recording\". It lets you always keep a recent trace available in case something goes wrong:cfg := trace.FlightRecorderConfig{\n    MinAge:   5 * time.Second,\n    MaxBytes: 3 << 20, // 3MB\n}\nrec := trace.NewFlightRecorder(cfg)\nrec.Start()\ndefer rec.Stop()\n#\nFinal thoughtsWe've covered a number of Go tools for writing concurrent programs:Goroutines for running concurrent tasks.Channels and select as flexible communication tools.Timers and tickers for working with time.Context for canceling operations.Wait groups for synchronizing goroutines.Mutexes to prevent race conditions.Condition variables for signaling events.Once for safe one-time initialization.Pools to reduce garbage collector load.Atomic operations.If you like the book, please recommend it to your friends or colleagues. If you're interested, check out my other books and projects.I'm glad you finished the book. Thank you, and I'll see you next time!\u2605\u00a0Subscribe to keep up with new posts."
                ],
                "output": "readability",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:30.307540+00:00",
                "status": "succeeded"
            }
        ],
        "screenshot": [],
        "singlefile": [],
        "title": [
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T13:38:12.247507+00:00",
                "index_texts": null,
                "output": "Go concurrency distilled",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:12.027331+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T10:09:49.553001+00:00",
                "index_texts": null,
                "output": "Go concurrency distilled",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:49.374114+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "curl",
                    "--silent",
                    "--location",
                    "--compressed",
                    "--max-time",
                    "120",
                    "--user-agent",
                    "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) curl/curl 8.14.1 (x86_64-pc-linux-gnu)",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "curl 8.14.1 (x86_64-pc-linux-gnu)",
                "end_ts": "2026-09-27T16:27:30.292500+00:00",
                "index_texts": null,
                "output": "Go concurrency distilled",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:30.087162+00:00",
                "status": "succeeded"
            }
        ],
        "wget": [
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--reject=js,mjs,woff,woff2,ttf,otf,eot,mp4,m4v,mov,webm,ogv,mkv,avi,flv",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "--timestamping",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-09-27T13:38:12.019758+00:00",
                "index_texts": null,
                "output": "IndexError: list index out of range",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T13:38:11.736161+00:00",
                "status": "failed"
            },
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--reject=js,mjs,woff,woff2,ttf,otf,eot,mp4,m4v,mov,webm,ogv,mkv,avi,flv",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "--timestamping",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-09-27T10:09:49.369441+00:00",
                "index_texts": null,
                "output": "antonz.org/go-concurrency-distilled/index.html",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T10:09:48.716871+00:00",
                "status": "succeeded"
            },
            {
                "cmd": [
                    "wget",
                    "--no-verbose",
                    "--adjust-extension",
                    "--convert-links",
                    "--force-directories",
                    "--span-hosts",
                    "--no-parent",
                    "-e",
                    "robots=off",
                    "--reject=js,mjs,woff,woff2,ttf,otf,eot,mp4,m4v,mov,webm,ogv,mkv,avi,flv",
                    "--timeout=120",
                    "--restrict-file-names=windows",
                    "--page-requisites",
                    "--user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36 ArchiveBox/0.7.4 (+https://github.com/ArchiveBox/ArchiveBox/) wget/GNU Wget 1.21.3",
                    "--compression=auto",
                    "--timestamping",
                    "https://antonz.org/go-concurrency-distilled/"
                ],
                "cmd_version": "GNU Wget 1.21.3",
                "end_ts": "2026-09-27T16:27:30.081195+00:00",
                "index_texts": null,
                "output": "IndexError: list index out of range",
                "pwd": "/data/archive/1790503447.653855",
                "schema": "ArchiveResult",
                "start_ts": "2026-09-27T16:27:29.802991+00:00",
                "status": "failed"
            }
        ]
    },
    "icons": null,
    "is_archived": true,
    "is_static": false,
    "latest": {
        "archive_org": null,
        "dom": null,
        "favicon": "favicon.ico",
        "git": null,
        "media": null,
        "pdf": null,
        "screenshot": null,
        "singlefile": null,
        "title": "Go concurrency distilled",
        "warc": null,
        "wget": "IndexError: list index out of range"
    },
    "link_dir": "/data/archive/1790503447.653855",
    "newest_archive_date": "2026-09-27T16:27:35.713889+00:00",
    "num_failures": 2,
    "num_outputs": 16,
    "oldest_archive_date": "2026-09-27T10:09:47.723091+00:00",
    "path": "/go-concurrency-distilled/",
    "schema": "Link",
    "scheme": "https",
    "snapshot_id": "67fb03eb-2f95-40c6-b3a1-d637be9b68fd",
    "sources": [
        "/data/sources/1790503447-import.txt"
    ],
    "tags": ",war-wiki",
    "tags_str": "war-wiki",
    "timestamp": "1790503447.653855",
    "title": "Go concurrency distilled",
    "updated": "2026-09-27T16:27:28.896184+00:00",
    "updated_date": "2026-09-27 16:27",
    "url": "https://antonz.org/go-concurrency-distilled/"
}