Backend Development 33 min read

Understanding Golang v1.0 Coroutine Implementation: Creation, Switching, and Runtime Mechanics

This article explains the internal workings of Golang v1.0 coroutines, covering their creation, stack‑switching logic, scheduler design, virtual‑memory basics, assembly examples, channel handling, socket event loops, and the low‑level implementation of defer, panic, and recover.

Xueersi Online School Tech Team
Xueersi Online School Tech Team
Xueersi Online School Tech Team
Understanding Golang v1.0 Coroutine Implementation: Creation, Switching, and Runtime Mechanics

The article uses Golang v1.0 as an example to explain the principles behind coroutine implementation, including coroutine creation, switching, termination, and the special g0 coroutine. Although v1.0’s scheduler is relatively simple, many factors can trigger scheduling, such as channel operations and socket event loops.

Problem Introduction

When you hear "goroutine" you may think of go func() , but the article asks what a coroutine really is, why it is considered a user‑space thread, and where its lightweight nature comes from.

Fundamental Supplements

To understand coroutines you need to know virtual memory, function stack frames, and basic assembly. The article shows a simple C program and its compiled assembly to illustrate how the compiler manages stack frames and registers.

int add(int x, int y) { return x+y; }
int main() { int sum = add(111,222); }

The resulting assembly demonstrates the use of %rbp and %rsp for stack management and how arguments are passed via registers ( %edi , %esi , %eax ).

Assembly Overview

Plan 9 assembly syntax used by Golang is introduced, with examples of common instructions such as MOVQ , ADDQ , SUBQ , JMP , and pseudo‑registers like FP and SP .

Goroutine Model (V1.0)

The classic MG model (M = OS thread, G = goroutine) is shown, followed by the simplified V1.0 model where only a global run‑queue exists and scheduling requires a lock.

Key Data Structures

struct G {
    int32 goid;
    byte* entry;
    byte* stack0;
    Gobuf sched;
    int16 status;
}
struct Gobuf {
    byte* sp;
    byte* pc;
    G* g;
}
enum {
    Gidle, Grunnable, Grunning, Gsyscall, Gwaiting, Gmoribund, Gdead
}

These structures hold the goroutine ID, entry function, stack pointer, scheduling context, and current state.

Goroutine Creation

The compiler translates go into a call to runtime.newproc , which eventually invokes runtime.newproc1 . The creation steps include allocating a G from the free list, allocating a stack (via runtime.malg ), initializing the Gobuf , and inserting the new goroutine into the run‑queue.

func newproc(siz int32, fn *funcval)

Important points: stack allocation must happen on the g0 stack, and the initial sp , pc , and entry fields are crucial for later switching.

g0 Stack and Scheduler Calls

When a goroutine needs to allocate a stack or perform scheduling, it switches to the g0 stack using runtime.mcall . The assembly for runtime.mcall saves the current context, checks whether the current goroutine is g0, and then calls the target function on the scheduler stack.

TEXT runtime·mcall(SB), $0
    MOVQ fn+0(FP), DI
    get_tls(CX)
    MOVQ g(CX), AX
    MOVQ 0(SP), BX
    MOVQ BX, (g_sched+gobuf_pc)(AX)
    LEAQ 8(SP), BX
    MOVQ BX, (g_sched+gobuf_sp)(AX)
    MOVQ AX, (g_sched+gobuf_g)(AX)
    MOVQ m(CX), BX
    MOVQ m_g0(BX), SI
    CMPQ SI, AX
    JNE 2(PC)
    CALL runtime·badmcall(SB)
    MOVQ SI, g(CX)
    MOVQ (g_sched+gobuf_sp)(SI), SP
    PUSHQ AX
    CALL DI
    POPQ AX
    CALL runtime·badmcall2(SB)
    RET

Goroutine Switching

The core switching routine is runtime.gogo , which restores sp and jumps to the saved pc . The first switch into a goroutine uses runtime.gogocall to set up the initial stack frame and call the entry function.

void runtime·gogo(Gobuf* gobuf, uintptr)
TEXT runtime·gogo(SB), $0
    MOVQ 16(SP), AX
    MOVQ 8(SP), BX
    MOVQ gobuf_g(BX), DX
    MOVQ 0(DX), CX
    get_tls(CX)
    MOVQ DX, g(CX)
    MOVQ gobuf_sp(BX), SP
    MOVQ gobuf_pc(BX), BX
    JMP BX

Scheduler and Run‑Queue

The scheduler runs on g0 and is invoked via runtime.mcall(schedule) . The schedule function updates the state of the yielding goroutine, puts it back into the run‑queue if necessary, selects the next runnable goroutine, and then either calls runtime.gogocall (for a fresh goroutine) or runtime.gogo (for a resumed one).

static void schedule(G *gp) {
    if (gp != nil) {
        switch(gp->status) {
        case Grunning:
            gp->status = Grunnable;
            gput(gp);
            break;
        case Gmoribund:
            gp->status = Gdead;
            gfput(gp);
            break;
        }
    }
    gp = nextgandunlock();
    gp->status = Grunning;
    if (gp->sched.pc == (byte*)runtime·goexit) {
        runtime·gogocall(&gp->sched, (void(*)(void))gp->entry);
    }
    runtime·gogo(&gp->sched, 0);
}

Channel Operations

Channels are implemented with the Hchan struct, containing a circular queue, send‑waiters, and receive‑waiters. Functions runtime.chansend and runtime.chanrecv may block the current goroutine by setting its status to Gwaiting and invoking runtime.gosched .

void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres) {
    if (c == nil) {
        g->status = Gwaiting;
        g->waitreason = "chan send (nil chan)";
        runtime·gosched();
        return;
    }
    if (c->dataqsiz > 0) {
        if (c->qcount >= c->dataqsiz) {
            g->status = Gwaiting;
            g->waitreason = "chan send";
            enqueue(&c->sendq, &mysg);
            runtime·unlock(c);
            runtime·gosched();
        }
    }
    // ...
}

Socket Event Loop (pollServer)

The runtime creates a pollServer that uses epoll/kqueue to wait for socket events. It runs in its own goroutine ( go s.Run() ) and wakes blocked goroutines by writing to a pipe.

type pollServer struct {
    pr, pw *os.File
    poll   *pollster
    deadline int64
}

func (s *pollServer) Run() {
    for {
        t := s.deadline
        fd, mode, err := s.poll.WaitFD(s, t)
        if fd < 0 {
            s.CheckDeadlines()
            continue
        }
        if fd == int(s.pr.Fd()) {
            // wake‑up pipe
        } else {
            netfd := s.LookupFD(fd, mode)
            s.WakeFD(netfd, mode, nil)
        }
    }
}

defer / panic / recover

The article explains that each goroutine has a G struct containing a defer list and a panic list. runtime.deferproc creates a defer record, links it at the head of the list (LIFO order), and stores the caller’s return address. When a panic occurs, runtime.panic walks the defer list, executes each defer via reflection, and if a defer calls recover , execution resumes at the saved return address.

uintptr runtime·deferproc(int32 siz, byte* fn, ...) {
    d = runtime·malloc(sizeof(*d) + siz - sizeof(d->args));
    d->fn = fn;
    d->siz = siz;
    d->pc = runtime·getcallerpc(&siz);
    d->link = g->defer;
    g->defer = d;
    return 0;
}

void runtime·panic(Eface e) {
    p = runtime·mal(sizeof *p);
    p->link = g->panic;
    g->panic = p;
    for (;;) {
        d = g->defer;
        if (d == nil) break;
        g->defer = d->link;
        g->ispanic = true;
        reflect·call(d->fn, d->args, d->siz);
        if (p->recovered) {
            d->link = g->defer;
            g->defer = d;
            runtime·mcall(recovery);
        }
    }
    runtime·startpanic();
    printpanics(g->panic);
    runtime·dopanic(0);
}

static void recovery(G *gp) {
    d = gp->defer;
    gp->defer = d->link;
    gp->sched.pc = d->pc;
    runtime·gogo(&gp->sched, 1);
}

Thus, a panic in one goroutine cannot be recovered by another, and the recovery mechanism restores execution to the point saved in the defer record.

Conclusion

By dissecting the Golang v1.0 source, the article provides a comprehensive view of how goroutines are created, scheduled, switched, and terminated, as well as how low‑level constructs such as channels, poll servers, and panic handling are built on top of these mechanisms.

ConcurrencygolangSchedulerruntimeassemblygoroutinepanicdefer
Xueersi Online School Tech Team
Written by

Xueersi Online School Tech Team

The Xueersi Online School Tech Team, dedicated to innovating and promoting internet education technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.