App QA
Pricing

Understanding Go Language: Interface Design, Goroutines, and Channels

The three core designs of the Go language: interface | goroutine | channel

Interface

Go is language-oriented towards interface programming, so the design of the interface is of great importance. The ingenuity of Go's interface design lies in the fact that an empty interface can be used as a "Duck" type, giving a static language like Go a certain degree of dynamism without sacrificing the compile-time type safety checks that static languages possess.

Source Code

From an implementation perspective, the interface is actually a structure containing two members. One of the member pointers points to an area containing type information, which can be understood as a virtual table pointer, while the other points to the specific data, which is the data actually referenced by the interface.

The structure of Itab is as follows:

The interface type contains some information about the interface itself_ Type refers to the specific implementation type, which will be described in detail in eface below. Bad is a state variable, and fun is an array of pointers with a length of 1. The function pointers corresponding to method will be saved after the address of fun [0]. There is a hash table in the go runtime package, through which you can obtain the itab. Link and inhash are used to save the corresponding location in the hash table and set the identity. The main codes are as follows:

The implementation of the null interface is slightly different. Any object in Go can be represented as interface {}, similar to void * in C, and there is type information in interface {}.

The structure of Type is as follows:

Tip: For more information about the interface, please refer to research!rsc: Go Data Structures: Interfaces

I_ Example

As for the application of interface, the following is a simple example about the interaction between Go and MySQL database.

First, create a task information table in the MySQL test library:

The most basic four operations of database interaction: add, delete, change and query. Here, take query as an example:

Go to query all data in this table

This code can realize the simple logic of table lookup, but there is a small problem that our table structure is relatively simple and only has four fields. If we change a table with 20+fields or more to query, this code is too inefficient. At this time, we can introduce interface {} to optimize it.

The optimized code is as follows:

Since interface {} can store any type of data, it can carry out batch reading and subsequent operations on data by constructing two arrays, args and values, where each value of args points to the address of the corresponding value of values. It is worth noting that Go is a strongly typed language, and different interfaces {} have different type information, so type conversion is required when performing assignment and other related operations.

Go also provides good support for MySQL transaction processing. General operations use the method of the db object, while transactions use the sql. Tx object. You can use the Begin method of db to create tx objects. The tx object also has Query, Exec and Prepare methods for database interaction, similar to the operation of db. After the query or modification is completed, you need to call Commit() commit or Rollback() rollback of the tx object.

For example, you need to update the user table created before using transactions. The code is as follows

Note: ":=" and "=" operators should not be confused

If transaction processing is not required, the code corresponding to update is as follows

It can be compared with the above code for adding transaction operations. Because the operation is relatively simple, a few lines of code have been added, and the db object has been replaced with a tx object.

Tip: For more information about Go's support for sql, please refer to the official documentation: https://golang.org/pkg/database/sql/

Goroutine

Concurrency: Dealing with different things at the same time

Parallel: doing different things at the same time

Go supports parallelism from the language level, and goroutine is the core of Go parallel design. In essence, goroutine is a coroutine. It has an independent call stack that can be managed by itself. You can understand goroutine as a lightweight thread. But the thread is scheduled by the operating system and preemptive. Goroutine is scheduled through its own scheduler.

Scheduler

Go's scheduler implements the G-P-M scheduling model, which has three important structures: M, P, G

M: Machine (OS thread)

P: Context (Go Scheduler)

G: Goroutine

The underlying data structure is as follows:

The interaction between M, P, and G can be illustrated through the following images from the Go runtime scheduler (http://morsmachine.dk/go-scheduler):

In the above diagram, there are two physical threads M, each with a context P and a running goroutine G. The gray Gs in the diagram are not running but are in a ready state, waiting to be scheduled. The runqueue queue is maintained by P.

In the diagram, M1 might be newly created or taken from a thread cache. When M0 returns, it must attempt to acquire a P to run G. In most cases, it will try to "steal" a P from another thread. If it fails, it will place G in a global runqueue and then be placed in the thread cache itself. All Ps will periodically check the global runqueue; otherwise, the Gs on the global runqueue will never be executed.

Another situation is when the tasks G allocated by P are quickly completed (due to uneven allocation), leading to some Ps being idle while the system is still running. However, if there are no more tasks G in the global runqueue, P has no choice but to take some Gs from other Ps to execute. In general, if P wants to steal a task from another P, it usually "steals" half of the runqueue, ensuring that each thread is used adequately.

How does P "steal" G from the queues maintained by other Ps? This involves the work-stealing algorithm. For more information about this algorithm, you can refer to: https://rakyll.org/scheduler/

G_ Example

Take a simple example to demonstrate how goroutine works

This code is very simple. Two different goroutines run asynchronously

The operation results are as follows:

Then make a small change, just swap the positions of the two functions in main(), and the rest of the code changes:

An interesting thing will happen:

The reason is also very simple, because when main() returns, it will not wait for other goroutines (non primary goroutines) to end. For the above example, after the main function executes the first say (), it creates a new goroutine and ends before executing the program, so the above running results will appear.

Channel

Goroutine runs in the same address space, so you must synchronize access to shared memory. The Go language provides a good communication mechanism channel to meet the data communication between goroutines. The channel is somewhat similar to the two-way pipeline in the Unix shell: you can send or receive values through it.

source code

The structure of waitq is as follows

You can see that a channel is actually a queue with a lock. Sendx and recvx can be regarded as producer and consumer queues. They store goroutines waiting for read operations on the channel and goroutines waiting for write operations on the channel, as shown in the following figure.

The specific implementation of writing channel (ch<- x) is as follows (only core code is selected):

It can be divided into three situations:

- If there is goroutine blocking on the channel, and the channel is empty, send the data directly to the goroutine.

- There is space available in changbuf: put the data in changbuf.

- Chanbuf has no space available: block the current goroutine.

The operation of reading channel (<- ch) is similar to that of sending, so no code will be displayed.

C_ Example

For a simple example of goroutine communicating with the channel, the logic is simple:

Here we define two channel jobs and results with cache. If you replace both channels with no cache, an error will be reported, but you can handle them in this way:

The more common channel operation is also select. When there are multiple channels, you can monitor the data flow on the channel through select.

Because both ch1 and ch2 are empty, case1 and case2 will not read successfully. Then select to execute the default statement.

Start QA Testing With WeTest

WeTest Quality Open Platform is the official one-stop testing service platform for game developers. We are a dedicated team of experts with more than ten years of experience in quality management. We are committed to the highest quality standards of game development and product quality and tested over 1,000 games.
WeTest integrates cutting-edge tools such as automated testing, compatibility testing, functionality testing, remote device, performance testing, and security testing, covering all testing stages of games throughout their entire life cycle.
Give it a try for free today. Start Trial!

Latest Posts
1Introducing WeTest UDT: A Tech-Driven Testing Solution WeTest UDT, the Unified Device Toolkit, offers a comprehensive suite of advanced cloud testing capabilities and unified device access solutions for enterprises.
2How To Improve Xbox Cloud Gaming Performance | Overview BOOST YOUR GAMING EXPERIENCE WITH THESE KEY TIPS FOR XBOX CLOUD GAMING.
3Common Issues Concerning CrashSight Integration and Reporting in Windows and Game Engines TACKLING THE NUANCES OF CRASHSIGHT IN WINDOWS AND VARIOUS GAME ENGINES.
4Common Issues Concerning CrashSight Integration and Reporting in Android NAVIGATE THROUGH THE HURDLES OF CRASHSIGHT INTEGRATION AND REPORTING ON ANDROID PLATFORMS.
5Comprehensive and cost-effective, why should you choose WeTest’s Compatibility Testing solution In the face of the global device fragmentation challenge, WeTest Compatibility Testing offers an all-in-one solution that covers a wide range of multi-dimensional software and hardware combinations