GoRoutine Quick Start: Multi-Scenario Execution Process Analysis (with Code + Flowchart)
As the core feature of the Go language, Goroutine is the key to lightweight concurrency. It is lighter than traditional operating system threads and has lower creation cost. The default initial stack is only 2KB and supports dynamic expansion, which allows developers to easily achieve high concurrency programming. For beginners who are new to Go concurrency, understanding the execution relationship between goroutine and the main thread (can be understood as a process or main coroutine) is the core difficulty of getting started.
This article will start from a basic requirement and extend 4 different scenarios (including the case where Goroutine is not enabled), and dismantle the gorou through complete code and visualization execution flow chart. The concurrent execution logic of tine and the main thread can help you quickly grasp the core usage and scheduling characteristics of goroutine, as well as the essential difference between ‘open coroutines’ and ‘no coroutines’.
A review of basic needs
Core requirements: start a goroutine in the main thread (can be understood as a process or main coroutine), and the coroutine outputs ‘hello every 1 second’ , world’, the main thread outputs ‘hello: golang’ every 1 second, and exits the program after 10 outputs, requiring both to execute at the same time.
First look at the complete code of the basic scenario, and then gradually extend other scenarios, focusing on the comparison scenario of ‘Goroutine is not enabled’:
package main
import (
"fmt"
"strconv"
"time"
)
// test 函数:goroutine执行的逻辑,每隔1秒输出hello,world
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world " + strconv.Itoa(i))
time.Sleep(time.Second) // 暂停1秒,模拟耗时操作
}
}
func main() {
// 开启goroutine:在go关键字后跟上要执行的函数,即可启动一个协程
go test()
// 主线程逻辑:每隔1秒输出hello:golang,执行10次
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
The core concept
Before analyzing the scenario, first clarify the two key knowledge points to avoid understanding the deviation, especially the key difference between ‘open coroutines’ and ‘unopened coroutines’:
- Main thread and coroutines: After the main function is started, a main thread will be created by default; the goroutine is started through the go keyword. The exit rule of the GO program is: after the main thread exits, all the coroutines will be forced to terminate, regardless of whether the execution is completed, this is the core logical basis of the next three ‘enabled coroutines’ scenarios; and When the coroutines are not opened (remove the go keyword), the ‘synchronous call’ will be executed, that is, the test function is executed first, and then the subsequent logic of the main function is executed, and there is no ‘concursion’.
- Concurrency execution essence: goroutine is managed by the scheduler (GMP model) at the Go runtime, rather than the operating system directly scheduled. In the GMP model, G (Goroutine) is the task carrier, M (Machine) is the operating system thread, and P (Processor) is a logical processor, which is responsible for coordinating the binding and scheduling of G and M, and steals the machine through work. The system implements load balancing, so that the main thread and the coroutines seem to be ‘execute at the same time’, but it is actually the result of the scheduler’s quick switching of execution rights; when goroutine is not enabled, only the main thread is executing, and the program is executed in ‘sequential execution’ logic, and there is no scheduling switching.
- The role of time.sleep: The most intuitive function of time.sleep(time.second) in the code is to slow down the program execution speed and facilitate the naked eye Observe the effect of parallel execution – after all, the scheduling and switching speed of the coroutine scheduling is extremely fast. In addition, its more critical role is to make the current coroutines (whether the main coroutines or sub-coroutines) actively and timely give up the CPU time slice to ensure that other coroutines can quickly obtain the right to execute. It should be noted that not using time.sleep does not necessarily have no other coroutine preemption rights, depending on the go version: go Before 1.14, only collaborative preemption is supported. When there is no blocking operation (such as time.sleep, channel operation), the current coroutines will always occupy the CPU, and other coroutines cannot be preempted; go 1.14 and later introduce asynchronous preemptive scheduling, which will force preemption of long-term operation coroutines, but the effect is not as stable as Time.sleep actively gives up, and cannot achieve the effect of ‘slow output, easy to observe’. When a coroutine is not started, the program is only executed by the main coroutines, and Sleep will only suspend the execution process of the main coroutines, and neither can implement parallel observation, and there is no ‘other coroutine preemption’ situation (because there are no other coroutines).
Detailed explanation of four scenarios (code + flowchart + execution analysis)
The following is divided into four scenarios, one by one analyzes the execution process of goroutine and the main thread, and the focus is to supplement ‘Gorouti is not enabled The scene of NE’ is contrasted with other scenarios that enable coroutines to help you intuitively understand the essential difference between ‘opening coroutines’ and ‘not opening coroutines’. Each scenario contains complete code, execution flow diagrams, and core analysis.
Scenario 1: test() executes 10 times, main() executes 10 times (basic scenario, open goroutine)
- complete code
Consistent with the basic requirements code, the core is that the number of executions of the coroutine and the main thread is the same, both 10 times, and each execution is sleeping for 1 second, and the coroutine is started through go test(). The execution result is as follows
/app/go-atguigu/goroutine-demo # go run main.go
main() hello,golang1
test () hello,world 1
main() hello,golang2
test () hello,world 2
test () hello,world 3
main() hello,golang3
main() hello,golang4
test () hello,world 4
test () hello,world 5
main() hello,golang5
test () hello,world 6
main() hello,golang6
main() hello,golang7
test () hello,world 7
test () hello,world 8
main() hello,golang8
main() hello,golang9
test () hello,world 9
test () hello,world 10
main() hello,golang10
/app/go-atguigu/goroutine-demo #
- execution flowchart
We use the flowchart to visualize the execution order of the two (the arrows indicate the execution right switching, and the execution right is regained after hibernation):
流程图 TD
A[程序启动] --> B[主协程启动,执行go test()]
B --> C[子协程test()启动,进入就绪态]
B --> D[主协程继续执行自身for循环]
C --> E[子协程输出hello,world 1]
D --> F[主协程输出hello:golang 1]
E --> G[子协程休眠1秒,让出CPU,进入等待态]
F --> H[主协程休眠1秒,让出CPU,进入等待态]
G --> I[子协程休眠结束,进入就绪态,等待调度]
H --> J[主协程休眠结束,进入就绪态,等待调度]
I --> K[调度器分配执行权,子协程输出hello,world 2]
J --> L[调度器分配执行权,主协程输出hello:golang 2]
K --> M[子协程休眠1秒]
L --> N[主协程休眠1秒]
M --> O[重复执行,直到两者都执行10次]
N --> O
O --> P[主协程执行完毕,退出]
O --> Q[子协程执行完毕,退出]
P --> R[程序终止]
Q --> R
- perform analysis
- Starting logic: After the program starts, the main thread executes go test() first, starts the coroutine (at this time, the coroutine enters the ready state, waiting for the schedule), and then the main thread continues to execute its own for loop.
- Concurrency logic: Since both have time.sleep(time.second), they will actively give up after each output CPU, the scheduler will alternately assign the execution rights to the main thread and coroutines, so the console will alternately output ‘hello, world x’ and ‘hello: golang x’ (the order may be slightly different, determined by the scheduler, which is a normal phenomenon).
- Exit logic: When both the main thread and the coroutines have executed 10 loops, both of them exit normally, and the program is terminated. Because the number of executions of the two is the same and the sleep time is the same, the high probability will be completed at the same time.
- Key Note: The output order is not absolutely fixed, because the scheduling of goroutine is controlled by the Go runtime, not our manual control – the scheduler will allocate the execution rights according to the system resources and task conditions. The main thread can output 2 times in a row, and the coroutine outputs 2 times in a row, but the overall rule of ‘alternate output every 1 second’ will be maintained, which is the embodiment of work stealing and preemptive scheduling in the GMP scheduling model.
Scenario 2: test() executes 20 times, main() executes 10 times
- complete code
Only the number of loops to modify the test function is 20 times, the main thread is still 10 times, the rest of the logic remains unchanged, and the coroutines are opened through go test():
package main
import (
"fmt"
"strconv"
"time"
)
// test()执行20次
func test() {
for i := 1; i <= 20; i++ {
fmt.Println("test() hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启goroutine
// main()执行10次
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
- execution flowchart
流程图 TD
A[程序启动] --> B[主协程启动,执行go test()]
B --> C[子协程test()启动,就绪态]
B --> D[主协程执行自身for循环]
C --> E[子协程输出hello,world 1]
D --> F[主协程输出hello:golang 1]
E --> G[子协程休眠1秒,让出CPU]
F --> H[主协程休眠1秒,让出CPU]
G --> I[子协程就绪,等待调度]
H --> J[主协程就绪,等待调度]
I --> K[子协程输出hello,world 2]
J --> L[主协程输出hello:golang 2]
K --> M[子协程休眠]
L --> N[主协程休眠]
M --> O[重复执行,直到主协程执行10次]
N --> O
O --> P[主协程执行完毕,退出]
P --> Q[子协程被强制终止(未执行完20次)]
Q --> R[程序终止]
- perform analysis
- The first 10 executions: exactly the same as scenario 1, the main thread and the coroutines are alternately output, sleep for 1 second after each output, and the scheduler alternately assigns the execution right.
- Key turning point: When the main thread executes the 10th loop, it will exit directly. At this time, the coroutine has only executed 10 times (the same number of execution times as the main thread, because each sleep time is the same), and there are 10 remaining unexecuted.
- Exit logic (emphasis): According to Go’s coroutine rules, the main thread is the ‘entry’ of the program. After the main thread exits, no matter whether the coroutine is executed or not, it will be forced to terminate. Therefore, the coroutine cannot continue to execute the remaining 10 outputs, and the program directly terminates. This is also the point that is easy to step on the pit in Go concurrent programming – if the coroutines are to be executed, you need to use synchronization mechanisms such as sync.waitGroup.
- Phenomenon verification: After running the code, the console will alternately output 10 groups of ‘Hello, World X’ and ‘Hello: Golang X’, and then the program will exit directly, and there will be no output of ‘Hello, World 11’ and later. The execution result is as follows
/app/go-atguigu/goroutine-demo # go run main.go
main() hello,golang1
test () hello,world 1
test () hello,world 2
main() hello,golang2
test () hello,world 3
main() hello,golang3
main() hello,golang4
test () hello,world 4
main() hello,golang5
test () hello,world 5
test () hello,world 6
main() hello,golang6
test () hello,world 7
main() hello,golang7
main() hello,golang8
test () hello,world 8
main() hello,golang9
test () hello,world 9
main() hello,golang10
test () hello,world 10
test () hello,world 11
/app/go-atguigu/goroutine-demo # go run main.go
main() hello,golang1
test () hello,world 1
test () hello,world 2
main() hello,golang2
main() hello,golang3
test () hello,world 3
test () hello,world 4
main() hello,golang4
main() hello,golang5
test () hello,world 5
test () hello,world 6
main() hello,golang6
main() hello,golang7
test () hello,world 7
main() hello,golang8
test () hello,world 8
main() hello,golang9
test () hello,world 9
main() hello,golang10
test () hello,world 10
/app/go-atguigu/goroutine-demo #
Note: One of the execution results outputs hello, world 11 . Small probability events, or in other words, the execution result will not output the output of ‘Hello, World 12’ and later. Here is a simple execution diagram 1

Scenario 3: test() executes 10 times, main() executes 100 times (the main thread executes more times, open goroutine)
- complete code
The number of loops to modify the main function is 100 times, and the coroutine is still 10 times, and the rest of the logic remains unchanged. The coroutines are opened through go test():
package main
import (
"fmt"
"strconv"
"time"
)
// test()执行10次
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启goroutine
// main()执行20次
for i := 1; i <= 20; i++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
- execution flowchart
流程图 TD
A[程序启动] --> B[主协程启动,执行go test()]
B --> C[子协程test()启动,就绪态]
B --> D[主协程执行自身for循环]
C --> E[子协程输出hello,world 1]
D --> F[主协程输出hello:golang 1]
E --> G[子协程休眠1秒,让出CPU]
F --> H[主协程休眠1秒,让出CPU]
G --> I[子协程就绪,等待调度]
H --> J[主协程就绪,等待调度]
I --> K[子协程输出hello,world 2]
J --> L[主协程输出hello:golang 2]
K --> M[子协程休眠]
L --> N[主协程休眠]
M --> O[重复执行,直到子协程执行10次]
N --> O
O --> P[子协程执行完毕,正常退出]
P --> Q[主协程继续执行剩余90次循环]
Q --> R[主协程执行完毕,退出]
R --> S[程序终止]
- perform analysis
- The first 10 executions: consistent with scenario 1 and scenario 2, the main thread and the coroutine are alternately output, the scheduler alternately assigns the execution right, and the two execute simultaneously.
- Key turning point: After the 10th cycle of the coroutine is executed, it will exit normally (after the coroutines are executed, it will be automatically recycled by the Go run and release resources). At this time, the main thread has only been executed 10 times, and the remaining 90 times have not been executed.
- Follow-up execution: After the coroutine exits, the main thread will continue to execute the remaining 90 loops. At this time, only the main thread is running in the program, and the console will continue to output ‘hello:golang X’ (X from 11 to 100) until the main thread is completed.
- Core difference: The exit of the coroutines will not affect the execution of the main thread – the main thread will be executed until the end of its own loop, and the program will be terminated. This is in stark contrast to Scenario 2. The core reason is: the main thread is the core of the program, and the coroutine is the ‘subjective task’ started by the main thread. The exit of the process does not affect the main thread, and the exit of the main thread will terminate all the coroutines, which is also determined by the coroutine life cycle management rules of GO. The execution result is as follows
/app/go-atguigu/goroutine-demo # go run main.go
main() hello,golang1
test () hello,world 1
main() hello,golang2
test () hello,world 2
main() hello,golang3
test () hello,world 3
main() hello,golang4
test () hello,world 4
main() hello,golang5
test () hello,world 5
main() hello,golang6
test () hello,world 6
main() hello,golang7
test () hello,world 7
main() hello,golang8
test () hello,world 8
main() hello,golang9
test () hello,world 9
main() hello,golang10
test () hello,world 10
main() hello,golang11
main() hello,golang12
main() hello,golang13
main() hello,golang14
main() hello,golang15
main() hello,golang16
main() hello,golang17
main() hello,golang18
main() hello,golang19
main() hello,golang20
/app/go-atguigu/goroutine-demo #
Scenario 4: test() executes 10 times, main() executes 10 times (goroutine is not enabled, remove the go keyword)
- complete code
Core modification: remove the go keyword, directly call the test() function, do not open the coroutine, the number of execution times of test() and main() is 10 times, the rest of the logic remains unchanged, and the focus is on the difference between ‘simultaneous execution’ and ‘concurrent execution’:
package main
import (
"fmt"
"strconv"
"time"
)
// test()执行10次,无go关键字,同步执行
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("test() hello,world " + strconv.Itoa(i))
time.Sleep(time.Second) // 暂停1秒,仅暂停当前执行流程
}
}
func main() {
test() // 去掉go关键字,不开启goroutine,同步调用test()
// 主线程逻辑:只有test()执行完毕后,才会执行此处
for i := 1; i <= 10; i++ {
fmt.Println("main() hello,golang " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
- execution flowchart
When the goroutine is not enabled, the program is advanced in the ‘sequential’ logic, and there is no execution right to switch. The flow diagram is as follows:
流程图 TD
A[程序启动] --> B[主协程启动,执行test()(无go关键字)]
B --> C[test()输出hello,world 1]
C --> D[test()休眠1秒,程序暂停]
D --> E[test()继续输出hello,world 2]
E --> F[test()休眠1秒]
F --> G[重复执行,直到test()执行完10次]
G --> H[test()执行完毕,主协程继续执行自身for循环]
H --> I[main()输出hello:golang 1]
I --> J[main()休眠1秒,程序暂停]
J --> K[main()输出hello:golang 2]
K --> L[main()休眠1秒]
L --> M[重复执行,直到main()执行完10次]
M --> N[主协程执行完毕,退出]
N --> O[程序终止]
Perform analysis (focus on the scenarios that start the coroutines)
- Starting logic: After the program starts, the main thread executes the test() directly (there is no go keyword). At this time, no coroutines are created, the program only has the main The thread is executing, and the test() function will be executed completely, and then the for loop of the main() function is executed, which belongs to ‘simultaneous call’.
- Execution logic (difference from the core of the open coroutine): no concurrency effect, the execution sequence is completely fixed – first output 10 times in a row ‘hello, world x’ (1 second interval), and the output of ‘hello:golang x’ will start to output after the test() function is fully executed, and there will be no ‘alternate output’. Because there is no coroutine, there is no scheduler switching execution right, and the program is executed in sequence in code writing.
- Exit logic: After the test() is executed 10 times, it exits normally, and the main thread continues to execute its own 10 loops. After the main() execution is completed, the program terminates. Different from the scenario of opening the coroutines, there is no situation where ‘the main thread exits early and the coroutine is terminated’, because there is no coroutine, all the logic is executed sequentially in the main thread.
- Core comparison summary: enable goroutine (plus go) → concurrent execution (the main thread and the coroutine are alternately executed, the scheduler assigns the execution right); goroutine is not enabled (go go) → Synchronous execution (the main thread executes test() sequentially, and there is no scheduling switch). This is the most basic use difference between goroutine, and it is also the key to understanding ‘concurrency’ and ‘synchronization’. The execution result is as follows
/app/go-atguigu/goroutine-demo # go run main.go
main() hello,golang1
test () hello,world 1
main() hello,golang2
test () hello,world 2
main() hello,golang3
test () hello,world 3
main() hello,golang4
test () hello,world 4
main() hello,golang5
test () hello,world 5
main() hello,golang6
test () hello,world 6
main() hello,golang7
test () hello,world 7
main() hello,golang8
test () hello,world 8
main() hello,golang9
test () hello,world 9
main() hello,golang10
test () hello,world 10
main() hello,golang11
main() hello,golang12
main() hello,golang13
main() hello,golang14
main() hello,golang15
main() hello,golang16
main() hello,golang17
main() hello,golang18
main() hello,golang19
main() hello,golang20
/app/go-atguigu/goroutine-demo #
4. Core Summary and Pit Avoidance Guide
Through the comparison of the above 4 scenarios (3 kinds of goroutines, 1 type is not enabled), we can summarize the goroutine The core characteristics, the essential difference between ‘opening and not opening’, and the key points of entry-level pitfalls can help you quickly grasp the use logic of goroutine:
- The difference between the core characteristics and the opening/unopened
- Lightweight and efficient: goroutine is dispatched by Go runtime, the initial stack is only 2KB, the creation and destruction cost is much lower than the operating system thread, and supports millions of concurrency;
- The difference between concurrency and synchronization: enable goroutine (go test()) → the main thread and the coroutine are executed concurrently, the scheduler assigns the execution rights through the work stealing mechanism, and alternate output occurs; goroutine (test()) → is not enabled Synchronous execution, the main thread first executes the test(), and then executes its own logic, the order is fixed, and there is no alternate output.
- Life cycle rules: When goroutine is turned on, the main thread dominant program exits (the main thread exits → Compulsory termination of the coroutine), the exit of the coroutine does not affect the main thread; when the goroutine is not enabled, there is no main/coroutine distinction, all logic is executed sequentially in the main thread, and the program terminates after execution.
- Getting Started Points
- Avoid ‘exit when the coroutine is not completed’: If the main thread is required to exit after the execution of the coroutine is completed, the ‘execution number of execution’ cannot be relied on, and SYNC needs to be used .WaitGroup (will be explained later), otherwise the main thread exiting early will cause the coroutine to be forced to terminate, resulting in the problem of unfinished tasks or resource leakage.
- Understand ‘scheduling randomness’: After the goroutine is turned on, the execution sequence is determined by the scheduler, not fixed alternation, even if the sleep time of the two in the code is the same, it is possible This is normal when continuous output occurs (especially when the CPU resources are tight), and the execution sequence is completely fixed and there is no randomness when the goroutine is not turned on.
- Use the non-blocking coroutine with caution: If there is no blocking operation such as time.sleep in the coroutines, the scheduler may make one coroutine execute the other (go 1.14+ supports preemptive scheduling, but it is still not recommended to rely on dependencies), you can actively make the CPU time slice through runtime.gosched() to ensure the concurrent effect; when goroutine is not turned on, there is no such problem.
- Distinguish the ‘go keywords’: the Go keyword is the core of opening goroutine, and removing Go will become an ordinary synchronous function call, which will lose the concurrency ability. This is the most confusing point for beginners. Be sure to use code to compare memory.
5. Follow-up learning direction
This article helps you master the basic usage and execution process of goroutine through 4 scenarios, as well as the essential difference between ‘opening coroutines’ and ‘not opening coroutines’, but this is just an entry to Go concurrency. Follow-up can continue to learn:
- sync.waitGroup: Implement the main thread to wait for the execution of the coroutines to complete, and solve the problem of ‘the coroutines have not been executed and terminated’.
- sync.mutex: Solve the ‘competitive condition’ problem when multiple goroutines operate the same variable at the same time, and avoid data confusion.
- Channel: The core mechanism of ‘communication is better than shared memory’ in the Go language, which realizes secure communication and synchronization between goroutines.
- In-depth GMP scheduling model: understand the collaborative working mechanism of G, M, and P, as well as the underlying principles of work theft and preemptive scheduling, and improve the performance optimization ability of concurrent programs.
Goroutine is the soul of the Go language, mastering its basic execution logic, and the difference between ‘on and unopened’, can lay a solid foundation for subsequent high-concurrency programming. It is recommended that you run the code in this article more, observe the output results in different scenarios, and understand the scheduling logic in combination with the flowchart, in order to truly understand the usage of goroutine~