[ $davids.sh ] โ€” david shekunts blog

๐Ÿ˜ฌ From Node.js to Go. v1 ๐Ÿ˜Ž

# [ $davids.sh ] ยท message #240

๐Ÿ˜ฌ From Node.js to Go. v1 ๐Ÿ˜Ž

Opening a series of articles about Go for Node.js developers and starting with asynchronous programming

#go #nodejs #typescript #top

  • @ [ $davids.sh ] ยท # 1442

    Node.js exposes one thread and operates asynchronous tasks within that thread: when we reach an I/O or syscall moment, Node queues the task for I/O/syscall completion and picks up another task with completed I/O/syscall. Each such task preserves its stack, closure references, and "position in code" from which to continue.

    Conditional comparison โ€“ this is how 1 CPU core works: it executes operations until it encounters I/O/syscall, then postpones the task until it reports "I have received all necessary data."

    Golang, on the other hand, creates a number of threads specified by the GOMAXPROCS env var, creating a "worker" ("processor") on each, an entity similar in fact to the entire Node.js, and similarly executes tasks (goroutines) until I/O/syscall and postpones continuation until readiness.

    And if we compare here, it's like the structure of a set of cores, where the scheduler distributes tasks among them, and each individual core processes or postpones tasks waiting for I/O/syscall.

    Important differences are that (1) goroutines are executed in parallel within a set of threads and sequentially within a thread, (2) Go can move goroutines between threads if one becomes free before others, (3) it has a preemptive scheduler that gives even a synchronous goroutine execution time no longer than 10ms, allowing you not to worry that long synchronous goroutines will block the execution of others.

    It's also interesting to note that a similar mechanism is a kind of "golden mean" in maximizing the execution of I/O intensive tasks, which can also be implemented in other languages, and the closest concept to goroutines is "green threads." Undoubtedly, there are cases where it's important for us to strictly pin tasks to specific threads and cores (e.g., thread per core), but this is already the prerogative of other languages.

    Among the similarities with Node is that we practically don't manage threads, concurrency, and parallelism in any way, except for a couple of rarely used mechanics.

    There are 2 main differences in programming in Node.js and Go, considering this mechanism: (1) if some data is mutably used between multiple goroutines, then we have every chance of a race condition, and therefore, we have to use various techniques for managing concurrency (atomics, locks, immutability, actors, etc.), (2) for safe communication between goroutines, channels must be used.

    Even in Node.js, you can encounter race conditions (for example, try to mutate the same data in Promise.all), but it's a rather rare case in Node, so we don't use it that often.

    Therefore, the main topics to take away after Node are: WaitGroup, ErrGroup, Atomic, RWLock, fan-in, fan-out, buffered vs unbuffered channels.

    I'll leave a couple of useful links (it's better to read them in order):

  • @ Arsen IT-K Arakelyan ยท # 1443

    I've been waiting for a guide from you on this path for a long time)