05 Things About the Executable Programs Part 2

05 Things about the Executable Programs - Part 2 #

In the previous article, I explained the meaning of code blocks. Code blocks in Go are nested, just like larger circles contain smaller circles.

A code block can have multiple sub-code blocks, but for each code block, there can be at most one code block directly containing it (which can be referred to as the outer code block).

This division of code blocks indirectly determines the scope of program entities. Let’s take a look at the relationship between them today.

Let me first explain what scope is. As we all know, a program entity is created to be referenced by other code. So, where can the code reference it? This involves its scope.

As I mentioned earlier, there are three types of access permissions for program entities at the package level: package-private, module-private, and public. This is actually the definition of the scope of program entities in Go at the language level based on code blocks.

Package-private and module-private access permissions correspond to code package code blocks, and public access permissions correspond to global code blocks. However, this granularity is relatively rough, and we often need to use code blocks to refine the scope of program entities.

For example, if I declare a variable within a function, normally, this variable cannot be referenced by code outside this function. Here, the function is a code block, and the scope of the variable is limited to that code block. Of course, there are exceptions to this, which I will explain when talking about functions.

In conclusion, please remember that the scope of a program entity is always limited to a certain code block, and the greatest use of this scope is to control the access permission of the program entity. The practice of the programming design principle “high cohesion, low coupling” can start exactly from here.

You should be able to further appreciate the charm of code blocks and scope through the following question.

Today’s question: What happens if a variable has the same name as a variable in its outer code block?

I saved the code for this question in the demo10.go file. You can find it in the puzzlers/article5/q1 package of the “Golang_Puzzlers” project.

package main

import "fmt"

var block = "package"

func main() {
	block := "function"
	{
		block := "inner"
		fmt.Printf("The block is %s.\n", block)
	}
	fmt.Printf("The block is %s.\n", block)
}

This source code file has four code blocks: the global code block, the code block represented by the main package, the code block represented by the main function, and a code block enclosed in curly braces within the main function.

I declare a variable named block in the latter three code blocks respectively, and assign the string values "package", "function", and "inner" to them. In addition, I attempt to print out “The block is %s.” at the end of the last two code blocks using the fmt.Printf function. The “%s” here is just a placeholder, and the program will replace it with the actual value of the block variable.

The specific question is: Can the code in this source code file compile? If not, what is the reason? If it can, what content will be printed after running it?

Typical Answer #

The code can be compiled. When executed, it will print:

The block is inner.
The block is function.

Problem Analysis #

At first glance, you might think that this question cannot be compiled because all three lines of code declare variables with the same name. Indeed, it is not possible to compile code that declares variables with the same name, except for using the short variable declaration to redeclare an existing variable, but that only applies within the same code block.

For different code blocks, having variables with the same name is not a problem, and the code can still be compiled. This is true even if these code blocks have a direct nesting relationship, just like the main package code block, the main function code block, and the innermost code block in demo10.go.

This rule is convenient and reasonable, otherwise we would be bothered every day with choosing variable names. However, it leads to another question: which variable am I referencing when I use the variable? This is the second point of this question.

This actually involves a very visual lookup process. This lookup process not only applies to variables, but also to any program entity, as shown below.

  • First, when the code references a variable, it always first looks for that variable in the current code block. Note that the “current code block” here refers only to the code block where the code referencing the variable is located and does not include any sub-code blocks.
  • Secondly, if the variable with the same name is not declared in the current code block, the program will look for it layer by layer starting from the code block directly containing the current code block, following the nesting relationship of the code blocks.
  • In general, the program will continue to search until it reaches the code block represented by the current code package. If it still cannot find the variable, the Go compiler will report an error.

Remember? If we import other code packages in the current source file, when referring to program entities in those packages, we need to use a qualifier as a prefix. So when the program is looking for a name (identifier) that represents an unqualified variable, it will not search in the imported code package.

But there is a special case. If the import statement of the code package is written in the form of import . "XXX" (note the dot in the middle), the program will treat the public program entities in the “XXX” package as program entities in the current source file.

For example, if there is an import statement import . fmt, then when we refer to the fmt.Printf function in the current source file, we can directly use Printf. In this special case, after searching in the current source file, the program will first search in the code packages imported in this way.

Okay, after you understand the above process, let’s take a look at the code in demo10.go again. Does it make more sense now?

From the perspective of scope, although the variable declared through var block = "package" has a scope of the entire main code package, it is “masked” by those two variables with the same name within the main function.

Similarly, although the variable block declared at the beginning of the main function has a scope of the entire main function, it cannot be referenced inside the innermost code block. Conversely, the block variable in the innermost code block cannot be referenced by code outside that block, which is the other reason why the second line of the output is “The block is function.”

Now you should know that this question seems simple, but it actually tests a broad and extensible range of knowledge.

Knowledge Expansion #

What is the difference between variables with the same name in different code blocks and variable redeclarations?

For convenience, let’s call variables with the same name in different code blocks “variables with the same name”. Note that it is not allowed to have variables with the same name in the same code block, as it violates the syntax of the Go language. We have already discussed the appearances and mechanisms of these two cases extensively. Can you think of some differences? Think about it and then look at the list below.

  1. The variables in a variable redeclaration must be within a specific code block. Note that “within a specific code block” here does not include any of its sub code blocks, otherwise it becomes “between multiple code blocks”. The variables with the same name refer to variables represented by the same identifier between multiple code blocks.
  2. Variable redeclaration refers to multiple declarations of the same variable, where there is only one variable. In the case of variables with the same name, there are multiple variables involved.
  3. Regardless of how many times the variable is redeclared, its type must always be consistent, following the type given when it was first declared. Variables with the same name do not have similar restrictions, and their types can be arbitrary.
  4. If there is a direct or indirect nesting relationship between the code blocks where variables with the same name are located, “shadowing” will definitely exist between them. However, this phenomenon will never occur in the case of variable redeclarations.

Of course, we have previously discussed that there are some prerequisites for variable redeclarations, but that is not the focus here, so I won’t go into detail.

Please pay attention to the third point among these four differences. Since the types of variables with the same name can be arbitrary, you need to be particularly careful when there is “shadowing” between them.

Different types of values generally have different characteristics and usages. When you perform operations on a value of a certain type that can only be done on values of other types, the Go compiler will definitely tell you “this is not allowed”.

This is a good thing, and it’s even worth celebrating, because the problem in your program has been discovered in advance. Otherwise, your program may cause very subtle problems during the running process, making you puzzled.

By comparison, the cost of debugging at that time would be too high. Therefore, we should try our best to use the syntax, specifications, and commands of the Go language to constrain our program.

Regarding the issue of variables with the same name but different types, let’s take a look at the source code file demo11.go in the puzzlers/article5/q2 package. It is a very typical example.

package main

import "fmt"

var container = []string{"zero", "one", "two"}

func main() {
	container := map[int]string{0: "zero", 1: "one", 2: "two"}
	fmt.Printf("The element is %q.\n", container[1])
}

In demo11.go, there are two variables named container, located in the code block of the main package and the code block of the main function, respectively. The variable in the code block of the main package is of type slice, and the other one is of type map. At the end of the main function, I attempt to print the element with index 1 from the container variable’s value.

If you are familiar with these two types, you will know that we can use indexing expressions on their values, such as container[0]. As long as the integer in the square brackets is within the valid range (here it is [0, 2]), it can retrieve a specific element from the value.

If the type of container is not an array, slice, or map type, the indexing expression will cause a compilation error. This is an example of Go language syntax helping us constrain the program; however, when we want to know the exact type of container, using the indexing expression is not enough.

When the values of variables with the same name are converted to values of some interface type, or when their types themselves are interface types, strict type checking becomes necessary. As for how to check, we will discuss it in the next article.

Summary #

We first discussed code blocks and their clever relationship with the scope of program entities and access control. The Go language itself provides relatively coarse-grained access control for program entities. However, we can use code blocks and scope to fine-tune control over them.

If there are variables with the same name in nested code blocks, we should be particularly careful as they may be “shadowed”. This means that when referencing variables in different code blocks, they may actually refer to different variables. The specific way to distinguish them depends on the process of looking up (representing program entities) identifiers in the Go language.

Also, remember the difference and important characteristics between variable redeclaration and variables with the same name. One point that can easily lead to subtle problems is that variables with the same name can have different types. In such cases, we should often check their types before using them. Using Go language’s syntax, conventions, and commands for assistance in checking is a good approach, but sometimes it is not sufficient.

Reflection Question #

When we were discussing the scope of identifiers in Go, we mentioned the import . XXX way of importing packages. Here’s a question for you to think about:

If variables in the imported package have the same name as variables in the current package, does Go consider them as “duplicated variables” or does it throw an error?

Actually, we can try it out with an example, but the key point is why? Please try to explain the answer based on code blocks and scopes.

Click here to view the detailed code for the Go Language column.