SoFunction
Updated on 2025-03-02

Explore advanced usage of switch in Go

Recently, when I opened the source code, I saw a very interesting usage of switch. Let me share it.

Note that what is discussed here is nottyped switch, that is, the type followed by the case statement.

Look directly at the code:

	func (s *systemd) Status() (Status, error) {

	exitCode, out, err := ("systemctl", "is-active", ())

	if exitCode == 0 && err != nil {

	return StatusUnknown, err

	}

	


	switch {

	case (out, "active"):

	return StatusRunning, nil

	case (out, "inactive"):

	// inactive can also mean its not installed, check unit files

	exitCode, out, err := ("systemctl", "list-unit-files", "-t", "service", ())

	if exitCode == 0 && err != nil {

	return StatusUnknown, err

	}

	if (out, ) {

	// unit file exists, installed but not running

	return StatusStopped, nil

	}

	// no unit file

	return StatusUnknown, ErrNotInstalled

	case (out, "activating"):

	return StatusRunning, nil

	case (out, "failed"):

	return StatusUnknown, ("service in failed state")

	default:

	return StatusUnknown, ErrNotInstalled

	}

	}

You can also find it here:Code link

A brief explanation of what this code is doing: call the systemctl command to check the running status of the specified service. The specific method is to filter the output of systemctl and then judge the current running status based on the prefix of the obtained string.

What's interesting is that this switch, first of all, there is no expression after it; secondly, after each case, there is a function call expression, and the return value is of type bool.

Although it looks weird, this code definitely has no syntax problems and can be compiled and passed; there are no semantic or logical problems, because the user is using it well, and this project has nearly 4,000 stars that are not something that everyone is not random.

I won't keep it a secret here, just announce the answer:

  • ifswitchThere is no expression after that, then it is equivalent to this:switch true
  • case expression pressFrom top to bottom From left to rightsequential evaluation;
  • If the value obtained by the expression after the case is the same as the value of the expression after the switch, then enter this branch and other cases are ignored (unless you use fallthrough, but this will jump directly into the branch of the next case and the expression on the next case will not be executed).

Then the above string of codes is easy to understand:

  • First of allswitch true, expect a case to find the value true;
  • Execute from top to bottom, if it is false, go down to the next case, if it is true, go to the branch of this case.

It is equivalent to the following paragraph:

	func (s *systemd) Status() (Status, error) {

	exitCode, out, err := ("systemctl", "is-active", ())

	if exitCode == 0 && err != nil {

	return StatusUnknown, err

	}

	


	if (out, "active") {

	return StatusRunning, nil

	}

	if (out, "inactive") {

	// inactive can also mean its not installed, check unit files

	exitCode, out, err := ("systemctl", "list-unit-files", "-t", "service", ())

	if exitCode == 0 && err != nil {

	return StatusUnknown, err

	}

	if (out, ) {

	// unit file exists, installed but not running

	return StatusStopped, nil

	}

	// no unit file

	return StatusUnknown, ErrNotInstalled

	}

	if (out, "activating") {

	return StatusRunning, nil

	}

	if (out, "failed") {

	return StatusUnknown, ("service in failed state")

	}

	


	return StatusUnknown, ErrNotInstalled

	}

It can be seen that it is difficult to say which of the two is better in terms of readability; both need to pay attention to putting common situations at the beginning to reduce unnecessary matching (the switch-case here cannot be directly jumped like when giving integer constants, and the actual execution is similar to the if statement given above).

So let's take a look at the generated code of the two. Usually I don't like to study the code generated by the compiler, but this time it is a small exception. How will the compiler deal with the two pieces of code that are very close to the execution process?

Let's make a simplified version example:

func status1(cmdOutput string, flag int) int {
	switch {
	case (cmdOutput, "active"):
	return 1
	case (cmdOutput, "inactive"):
	if flag > 0 {
	return 2
	}
	return -1
	case (cmdOutput, "activating"):
	return 1
	case (cmdOutput, "failed"):
	return -1
	default:
	return -2
	}
	}
	func status2(cmdOutput string, flag int) int {
	if (cmdOutput, "active") {
	return 1
	}
	if (cmdOutput, "inactive") {
	if flag > 0 {
	return 2
	}
	return -1
	}
	if (cmdOutput, "activating") {
	return 1
	}
	if (cmdOutput, "failed") {
	return -1
	}
	return -2
	}

Here is a compilation of the switch version:

	main_status1_pc0:

	TEXT main.status1(SB), ABIInternal, $40-24

	CMPQ SP, 16(R14)

	PCDATA $0, $-2

	JLS main_status1_pc273

	PCDATA $0, $-1

	SUBQ $40, SP

	MOVQ BP, 32(SP)

	LEAQ 32(SP), BP

	FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)

	FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)

	FUNCDATA $5, main.status1.arginfo1(SB)

	FUNCDATA $6, main.(SB)

	PCDATA $3, $1

	MOVQ CX, +64(SP)

	MOVQ AX, +48(SP)

	MOVQ BX, +56(SP)

	PCDATA $3, $-1

	MOVL $6, DI

	LEAQ go:string."active"(SB), CX

	PCDATA $1, $0

	CALL (SB)

	NOP

	TESTB AL, AL

	JNE main_status1_pc258

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."inactive"(SB), CX

	MOVL $8, DI

	NOP

	CALL (SB)

	TESTB AL, AL

	JEQ main_status1_pc147

	MOVQ +64(SP), CX

	TESTQ CX, CX

	JLE main_status1_pc130

	MOVL $2, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc130:

	MOVQ $-1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc147:

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."activating"(SB), CX

	MOVL $10, DI

	CALL (SB)

	TESTB AL, AL

	JNE main_status1_pc243

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."failed"(SB), CX

	MOVL $6, DI

	PCDATA $1, $1

	CALL (SB)

	TESTB AL, AL

	JEQ main_status1_pc226

	MOVQ $-1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc226:

	MOVQ $-2, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc243:

	MOVL $1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc258:

	MOVL $1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status1_pc273:

	NOP

	PCDATA $1, $-1

	PCDATA $0, $-2

	MOVQ AX, 8(SP)

	MOVQ BX, 16(SP)

	MOVQ CX, 24(SP)

	CALL runtime.morestack_noctxt(SB)

	MOVQ 8(SP), AX

	MOVQ 16(SP), BX

	MOVQ 24(SP), CX

	PCDATA $0, $-1

	JMP main_status1_pc0

I turned off the inline, otherwise the things inlined by hasprefix will make the entire assembly code difficult to read.

The above code is still easy to understand. The cases of "active" and "inactive" are put together. If they match, they will jump to the corresponding branch; the cases of "active" and "failed" are also put together. The operations after matching are the same as the previous two cases (in fact, the above two cases will jump to these two after the matching is executed. As for why you need to jump once more, I haven't delved into it. It may be to improve it.L1dThe hit rate, a large piece of instruction may cause the cache to be unable to be put, and thus pay the cost of updating the cache. With pipeline optimization, the overhead of a jmp may be lower than the penalty for cache miss, but this is difficult to measure in practice, so it is OK if I am talking to myself). The last string of statement blocks with ret is the corresponding case branch.

Let's take a look at the if code:

	main_status2_pc0:

	TEXT main.status2(SB), ABIInternal, $40-24

	CMPQ SP, 16(R14)

	PCDATA $0, $-2

	JLS main_status2_pc273

	PCDATA $0, $-1

	SUBQ $40, SP

	MOVQ BP, 32(SP)

	LEAQ 32(SP), BP

	FUNCDATA $0, gclocals·wgcWObbY2HYnK2SU/U22lA==(SB)

	FUNCDATA $1, gclocals·J5F+7Qw7O7ve2QcWC7DpeQ==(SB)

	FUNCDATA $5, main.status2.arginfo1(SB)

	FUNCDATA $6, main.(SB)

	PCDATA $3, $1

	MOVQ CX, +64(SP)

	MOVQ AX, +48(SP)

	MOVQ BX, +56(SP)

	PCDATA $3, $-1

	MOVL $6, DI

	LEAQ go:string."active"(SB), CX

	PCDATA $1, $0

	CALL (SB)

	NOP

	TESTB AL, AL

	JNE main_status2_pc258

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."inactive"(SB), CX

	MOVL $8, DI

	NOP

	CALL (SB)

	TESTB AL, AL

	JEQ main_status2_pc147

	MOVQ +64(SP), CX

	TESTQ CX, CX

	JLE main_status2_pc130

	MOVL $2, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc130:

	MOVQ $-1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc147:

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."activating"(SB), CX

	MOVL $10, DI

	CALL (SB)

	TESTB AL, AL

	JNE main_status2_pc243

	MOVQ +48(SP), AX

	MOVQ +56(SP), BX

	LEAQ go:string."failed"(SB), CX

	MOVL $6, DI

	PCDATA $1, $1

	CALL (SB)

	TESTB AL, AL

	JEQ main_status2_pc226

	MOVQ $-1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc226:

	MOVQ $-2, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc243:

	MOVL $1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc258:

	MOVL $1, AX

	MOVQ 32(SP), BP

	ADDQ $40, SP

	RET

	main_status2_pc273:

	NOP

	PCDATA $1, $-1

	PCDATA $0, $-2

	MOVQ AX, 8(SP)

	MOVQ BX, 16(SP)

	MOVQ CX, 24(SP)

	CALL runtime.morestack_noctxt(SB)

	MOVQ 8(SP), AX

	MOVQ 16(SP), BX

	MOVQ 24(SP), CX

	PCDATA $0, $-1

	JMP main_status2_pc0

Except for the different function names, the others are exactly the same. It can be said that there is no difference between the two in generating code.

You can see the code and their compiled products here:Compiler Explorer

Since the generated code is the same, there is no need to measure the performance, because it must be the same.

Finally, let’s summarize this infrequently used switch writing method, the form is as follows:

	switch {

	case expression1: // If true
	do works1

	case expression2: // If true
	do works2

	default:

	NonetrueWill be here

	}

Considering that this does not have any advantages in performance, and for those who first see this writing method, it may not be possible to understand its meaning very quickly, so I can think of only one scenario for using this writing method:

If your data has more than 2 fixed prefixes/suffixes/schemas, because it is impossible to use fixed constants to represent this situation, then adding a simple expression (function call or something) with case will be more compact than using if and can better express semantics. The more cases the more obvious the effect. For example, the example I gave at the beginning.

If your code does not meet the above situation, it would be better to be honest and practical if.

Having said that, although you don’t have the chance to write such a switch statement, it’s best to understand it, otherwise you will have to stare at it next time.

refer to

/ref/spec#Switch_statements

This is the end of this article about exploring the advanced usage of switches in Go. For more information about the unusual switch usage in Go, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!