In-depth analysis of Go language features

Abstract: This article was originally published on CSDN by the technical team of Grape City. Please indicate the source of the reprint: Grape City official website , Grape City provides developers with professional development tools, solutions and services to empower developers.

foreword

This article mainly introduces the characteristics of the GO language from 7 aspects of value passing and pointers, strings, arrays, slices, collections, object-oriented (encapsulation, inheritance, abstraction) and design philosophy.

Article directory:

1. Past and present life of Go

1.1 The process of the birth of the Go language

1.2 Step by step forming

1.3 officially released

1.4 Go Installation Guide

2. Special language features of GO language

2.1 Passing by value and pointers

2.2 Strings

2.3 Arrays

2.4 Slicing

2.5 Collection

2.6 Object Oriented

  2.6.1 Packaging

  2.6.2 Inheritance

  2.6.3 Abstraction

2.7 Design Philosophy

  2.7.1. Go has no default type conversion

  2.7.2. Go has no default parameters, nor method overloading

  2.7.3. Go does not support Attribute

  2.7.4. Go has no Exceptions

1. Past and present life of Go

1.1 The process of the birth of the Go language

It is said that as early as one day in September 2007, Google engineer Rob Pike started the construction of a C++ project as usual. According to his previous experience, this construction should last about 1 hour. At this time, he and two other Google colleagues, Ken Thompson and Robert Griesemer, began to complain and expressed their idea of ​​​​a new language. At that time, Google mainly used C++ to build various systems internally, but the complexity of C++ and the lack of native support for concurrency made the three big bosses very distressed.

The small talk on the first day was fruitful, and they quickly conceived a new language that could bring joy to programmers, match future hardware development trends, and satisfy Google's internal large-scale network services. And on the second day, they met again and began to seriously conceive the new language. After the meeting the next day, Robert Griesemer sent the following email:

It can be seen from the email that their expectations for this new language are: **On the basis of the C language, modify some errors, delete some criticized features, and add some missing functions. **Such as repairing the Switch statement, adding an import statement, adding garbage collection, supporting interfaces, etc. And this email became the first draft of Go's design.

A few days after this, Rob Pike came up with the name Go for the new language on a drive home. In his mind, the word "Go" is short, easy to type and can easily be combined with other letters after it, such as Go's tool chain: goc compiler, goa assembler, gol linker, etc., and this word also fits Their original intention for the language design: simple.

1.2 Step by step forming

After unifying the design ideas of Go, the Go language officially started the design iteration and implementation of the language. In 2008, Ken Thompson, the father of the C language, implemented the first version of the Go compiler. This version of the Go compiler is still developed in the C language. Its main working principle is to compile Go into C, and then Compile C into binaries. By mid-2008, the first version of Go's design was largely complete. At this time, Ian Lance Taylor, who also worked at Google, implemented a gcc front-end for the Go language, which is also the second compiler of the Go language. This achievement of Ian Taylor is not only an encouragement, but also a proof of the feasibility of Go, a new language. With a second implementation of the language, it is also important to establish Go's language specification and standard library. Subsequently, Ian Taylor officially joined the Go language development team as the fourth member of the team, and later became one of the core figures in the design and implementation of the Go language. Russ Cox is the fifth member of the Go core development team, also joining in 2008. After joining the team, Ross Cox cleverly designed the HandlerFunc type of the http package by taking advantage of the feature that the function type is a "first-class citizen", and it can also have its own methods. In this way, we can make an ordinary function a type that satisfies the http.Handler interface through explicit transformation. Not only that, Ross Cox also proposed some more general ideas based on the design at that time, such as the io.Reader and io.Writer interfaces, which established the I/O structure model of the Go language. Later, Ross Cox became the head of the Go core technical team, promoting the continuous evolution of the Go language. At this point, the initial core team of the Go language is formed, and the Go language has embarked on a path of stable evolution.

1.3 officially released

On October 30, 2009, Rob Parker gave a speech on the Go language at Google Techtalk, which was the first time that the Go language was made public. Ten days later, on November 10, 2009, Google officially announced that the Go language project was open source, and this day was officially designated by Go as the birth day of the Go language.

(Go language mascot Gopher)

1.4. Go installation guide

1. Go language installation package download

Go official website: https://golang.google.cn/

Just select the corresponding installation version (it is recommended to select the .msi file).

2. Check whether the installation is successful + whether the environment is configured successfully

Open the command line: win + R to open the run box, enter the cmd command, and open the command line window.

Enter go version on the command line to view the installed version. If the following content is displayed, the installation is successful.

[External link picture transfer failed, the source site may have an anti-leeching mechanism, it is recommended to save the picture and upload it directly (img-nhDTdQKK-1689067489257)(media/54576b6c7593e8c73a97975be0910b4b.png)]

2. Special language features of Go language

2.1 Passing by value and pointers

Function parameters and return values ​​in Go are all passed by value. What does that mean? For example the following code:

type People struct {
    
      
 name string  
}  

 func ensureName(p People) {
    
      
 p.name = "jeffery"  
 }  

 func main() {
    
      
 p := People{
    
      
 name: ""  
 }  
 ensurePeople(p)  
 fmt.Println(p.name) // 输出:""  
 }

Why doesn't the above code change the content of p to "jeffery"? Because of the pass-by-value feature of the Go language, the p received in the ensureName function is already a copy of p in the main function. This is the same result obtained by changing p to an int type in C#.

How to solve it? Use a pointer .

I don’t know how other people are. When I first learned Go and found out that I needed to learn pointers, I instantly recalled the painful memories of being tortured by C and C++ pointers in college, so I instinctively felt a sense of rejection of pointers. , although pointers can also be used in C#, if you don't write the underlying code, you may write the code less than once in 10 years.

Fortunately, the use of pointers in Go is simplified, there is no complicated pointer calculation logic, and pointers can be easily used by only knowing two operations:

  • "*": Get the content in the address
  • "&": get the address of the variable
var p \*People = \&People{
    
      
 name: "jeffery",  
 }

In the above code, I created a new People instance, obtained its address through the "&" operation, and assigned its address to a pointer type variable p of *People. At this time, p is a pointer type. According to C or C++, I cannot directly operate the field name in People, but Go simplifies the pointer operation. I can directly operate the fields in a pointer type variable. for example:

func main() {
    
      
 fmt.Println(p.name) // 输出:jeffery  
 fmt.Println(\*(p).name) // 输出:jeffery  
}

The above two operations are equivalent.

With pointers, we can easily simulate the C# code that passes parameters by reference :

type People struct {
    
      
 name string  
 }  

 func ensureName(p \*People) {
    
      
 p.name = "jeffery"  
 }  

 func main() {
    
      
 p := \&People{
    
      
 name: ""  
 }  
 ensurePeople(p)  
 fmt.Println(p.name) // 输出:jeffery  
 }

2.2 Strings

In C#, the string is actually an array of char type, which is a special reference type allocated in the stack space.

In Go language, strings are value types , and strings are a whole . That is to say, we cannot modify the content of the string. This concept can be clearly seen from the following example:

var str = "jeffery";  
str[0] = 'J';  
 Console.WriteLine(str); // 输出:Jeffery

The above syntax is valid in C#, because what we modify is actually a char type in the string, and such syntax in Go will be reported by the compiler as an error:

str := "jeffery"  
str[0] = 'J' // 编译错误:Cannot assign to str[0]

But we can use the array index to read the value of the corresponding string:

s := str[0]  
fmt.Printf("%T", s) // uint8

You can see that the return value is uint8, why is this? In fact, in Go, the string type is composed of a type called rune. When you enter the Go source code, you can see that the definition of rune is an int64 type. This is because in Go, strings are compiled into UTF8 codes one by one, and each rune is actually a value corresponding to the UTF8 code.

In addition, the string type has a pit:

str := "李正龙"  
fmt.Printf("%d", len(str))

The len() function is also a built-in function of go, which is used to find the length of the collection.

The above example will return 9, because Chinese will be compiled into UTF-8 encoding in Go, and the encoding length of a Chinese character is 3, so three Chinese characters will become 9, but it is not necessarily, because some special Chinese characters may It occupies 4 lengths, so you cannot simply use len() / 3 to get the text length.

Therefore, the method for finding the length of Chinese characters should be as follows:

fmt.Println(utf8.RuneCountInString("李正龙"))

2.3 Arrays

Arrays in Go are also a concept that I think is a bit too low-level. The basic usage is the same as C#, but the details are still very different.

First of all, Go's array is also a value type . In addition, because it "strictly" follows the concept that an array is a contiguous memory combination, the length of the array is a part of the array . This concept is also important, as this is a feature that directly distinguishes slices. Moreover, the length of an array in Go can only be a constant.

a := [5]int{
    
    1,2,3,4,5}  
b := [...]{
    
    1,2,3,4,5}  

lena := len(a)  
lenb := len(b)

The above are two relatively conventional initialization syntaxes for arrays in Go. The length of an array is the same as that of a string, and is obtained through the len() built-in function. The rest of the usage is basically the same as C#, for example, values ​​can be assigned by index, it can be traversed, and values ​​cannot be inserted.

2.4 Slicing

A concept corresponding to arrays is the unique Slice type in Go. Arrays are rarely used in daily development, because arrays have no scalability. For example, we hardly use arrays in C#, and List<T> is basically used where arrays can be used. Slice is a Go language implementation of List. It is a reference type , and its main purpose is to solve the problem that arrays cannot insert data. Its bottom layer is also an array, but it encapsulates the array and adds two pointers pointing to the left and right boundaries of the array, which makes Slice have the function of adding data.

s1 := []int{
    
    1,2,3,4,5}  
s2 := s1[1:3]  
s3 := make([]int, 0, 5)

The above are the three commonly used initialization methods of Slice.

  1. You can see that the only difference between slices and arrays is that there is no quantity in the array definition
  2. You can create another slice based on one deslicing, and the meaning of the number behind it is the current industry-wide left-inclusive right-closed
  3. A slice can be created by the **make()** function

The make() function feels like it can accompany a Go developer's life. The three reference types of Go are all initialized and created through the make function. For slices, the first parameter indicates the slice type, for example, the above example initializes a slice of int type, the second parameter indicates the length of the slice, and the third parameter indicates the capacity of the slice.

If you want to insert data into slices, you need to use the append() function, and the syntax is very weird, which can be said to be outrageous:

s := make([]int)  
s = append(s, 12345) // 这种追加值还需要返回给原集合的语法真不知道是哪个小天才想到的

A new concept emerges here, the capacity of slices . We know that arrays do not have the concept of capacity (in fact, there is, but the capacity is the length), and the capacity of slices is actually similar to the capacity of List<T> in C# (I know that most C#ers are using List Sometimes you don’t care about the Capacity parameter at all), and the capacity indicates the length of the underlying array.

The capacity can be obtained by the cap() function

In C#, if the data of the List fills the underlying array, an expansion operation will occur, and a new array needs to be created to copy the original data to the new array. This is a very performance-consuming operation, and the same is true in Go. . Therefore, when using List or slice in daily development, if the capacity can be determined in advance, it is best to define it during initialization to avoid performance loss caused by expansion.

2.5 Collection

In addition to built-in List as a slice in Go, it also built-in Dictionary<TKey, TValue> as a map type. map is the second of the three reference types in Go . It is created in the same way as a slice, and it also needs to pass the make function:

m := make(map[int]string, 10)

From the literal meaning, we can know that this sentence creates a map type whose key is int, value is string, and the initial capacity is 10.

The operation on map is not as complicated as C#, get, set and contains operations are all implemented through []:

m := make(map[string]string, 5)  

// 判断是否存在  
 v, ok := m["aab"]  
 if !ok {
    
      
 //说明map中没有对应的key  
 }  
// set值,如果存在重复key则会直接替换  
 m["aab"] = "hello"  

// 删除值  
 delete(m, "aab")

Here is a pitfall. Although the map in Go can also be traversed, Go forces the results to be out of order, so each time you traverse, you may not get the results in the same order.

2.6 Object Oriented

2.6.1 Packaging

Finally it comes to object-oriented. Careful students must have seen that there are no encapsulation control keywords public, protected and private in Go! So what about the encapsulation of my object-oriented first principle?

The encapsulation of the Go language is controlled by the case of the first letter of the variable (for me, a severe code cleanliness patient, this is a great boon, I no longer need to see those attributes with lowercase letters).

// struct类型的首字母大写了,说明可以在包外访问  
 type People struct {
    
      
 // Name字段首字母也大写了,同理包外可访问  
 Name string  
 // age首字母小写了,就是一个包内字段  
 age int  
}  

 // New函数大写了,包外可以调到  
 func NewPeople() People {
    
      
 return People{
    
      
 Name: "jeffery",  
 age: 28  
 }  
 }

2.6.2 Inheritance

Encapsulation is done, how about inheritance? There seems to be no inherited keyword extends in Go? Go completely designed the logic of reuse based on the design idea of ​​prioritizing composition rather than inheritance in the design pattern. There is no inheritance in Go , only composition .

type Animal struct {
    
      
 Age int  
 Name string  
}  

 type Human struct {
    
      
 Animal // 如果默认不定义字段的字段名,那Go会默认把组合的类型名定义为字段名  
 // 这样写等同于: Animal Animal  
 Name string  
}  

func do() {
    
      
 h := \&Human{
    
      
 Animal: Animal{
    
    Age: 19, Name: "dog"},  
 Name: "jeffery",  
 }  
 h.Age = 20  
 fmt.Println(h.Age) // 输出:20,可以看到如果自身没有组合结构体相同的字段,那可以省略子结构体的调用直接获取属性  
 fmt.Println(h.Name) // 输出:jeffery,对于有相同的属性,优先输出自身的,这也是多态的一种体现  
 fmt.Println(h.Animal.Name)// 输出:dog,同时,所组合的结构体的属性也不会被改变  
 }

This combined design pattern greatly reduces the coupling brought about by inheritance. In this regard alone, I think it is the perfect silver bullet.

2.6.3 Abstraction

We have already seen in the part of explaining keywords that Go has interfaces, but there is also no implemented keyword for implementing interfaces. That is because all interfaces in Go are implicitly implemented .

type IHello interface {
    
      
 sayHello()  
 }  

 type People struct {
    
    }  

 func (p \*People) sayHello() {
    
      
 fmt.Println("hello")  
 }  

 func doSayHello(h IHello) {
    
      
 h.sayHello()  
 }  

 func main() {
    
      
 p := \&People{
    
    }  
 doSayHello(p) // 输出:hello  
 }

It can be seen that the structure p in the above example has nothing to do with the interface, but it can be referenced by the doSayHello function normally, mainly because all interfaces in Go are implicitly implemented. (So ​​I think it is really possible for you to write and suddenly implement a certain interface of a certain dependent package)

In addition, here we see a different syntax. After the function keyword func, the function name is not directly defined, but a pointer to a structure p is added. Such a function is a function of a structure, or more straightforwardly, a method in C#.

By default, we all use the pointer type to define functions for the structure. Of course, pointers can also be used without pointers, but in that case, the content changed by the function is completely irrelevant to the original structure. Therefore, it generally follows the principle of using pointers without thinking .

Well, we have encapsulation, inheritance, and abstraction. As for polymorphism, we have already seen it in inheritance. Go also matches the same function of itself first. If there is no function, it will call the function of the parent structure. Therefore, by default, all functions are is the overridden function.

2.7 Design Philosophy

The design philosophy of the Go language is less is more. What this sentence means is that Go needs simple syntax, and simple syntax also includes explicit over implicit (interface types are really full of question marks). What does it mean?

2.7.1. Go has no default type conversion

var i int8 = 1  
var j int  
j = i // 编译报错:Cannot use 'i' (type int8) as the type int

Another example is that the string type cannot be concatenated with other types such as int by default. For example, inputting "n hello" + 1 will also report a compilation error in Go. The reason is that the designers of Go think that these are all implicit conversions, and Go needs to be simple and should not have these.

2.7.2. Go has no default parameters, nor method overloading

This is also a very annoying language feature. Because overloading is not supported, when writing code, you have to write a large number of functions that may be repeated but have different names. Some developers specifically asked Go designers about this feature, and the reply given was that the design goal of Go is simplicity. Under the premise of simplicity, some redundant codes are acceptable.

2.7.3. Go does not support Attribute

Unlike the current lack of generics, Go's generics are a feature under development and have not had time to do so. The feature Attribute is the annotation in Java, and it is a language feature that is clearly stated that it will not be supported in Go.

How powerful can annotations be in Java? As an example:

In the era when the large-scale Internet is turning to the microservice architecture, distributed multi-segment commits and distributed transactions are relatively large technical barriers. Taking distributed transactions as an example, multiple microservices may not be developed by the same team, and may also be deployed around the world. If an operation needs to be rolled back, all other microservices need to implement a rollback mechanism. This involves not only complex business models, but also more complex database rollback strategies (what 2PC, TCC, each strategy can be taught as a separate class).

If this kind of thing is to be developed from scratch, it is almost difficult to consider it comprehensively. Not to mention that such complex code is coupled to business code, and the code will become very ugly. Not to mention distributed transactions. A simple memory cache is very confusing for us. In the code, we often see the code that reads the cache first and then reads the database. It is completely coupled with the business and cannot be maintained at all.

In Spring Cloud, code users can use a simple annotation (that is, the feature of C#) @Transactional, then this method supports transactions, so that this complex technical-level code is completely decoupled from business code , developers It is enough to write the business code completely according to the normal business logic, and there is no need to worry about some problems of the transaction.

However, the designers of Go also believe that annotations will seriously affect the minds of code users on a call, because adding an annotation can lead to a completely different function of a function, which is in line with Go's design philosophy that explicit is greater than implicit Contrary to it, it will seriously increase the mental burden of users, and it does not conform to Go's design philosophy (hey, it's outrageous...)

2.7.4. Go has no Exceptions

There is no concept of exception in Go, instead an error mechanism is provided. For C#, if there is a problem with a piece of code running, then we can manually throw an Exception, and the caller can catch the corresponding exception for subsequent processing. However, there are no exceptions in Go, and the alternative is the error mechanism. What is the error mechanism? Remember how I said earlier that almost all functions in Go have multiple return values? Why so many return values? Yes, it is for receiving error. For example the following code:

func sayHello(name string) error {
    
      
 if name == "" {
    
      
 return errors.New("name can not be empty")  
 }  
 fmt.Printf("hello, %s\\n", name)  
 return nil  
}  

// invoker  
func main() {
    
      
 if err := sayHello("jeffery"); err != nil {
    
      
 // handle error  
 }  
}

Such an error mechanism needs to ensure that all codes will not crash abnormally during the running process. Whether each function is executed successfully or not needs to be judged by the error information returned by the function. If the error returned by a function call == nil, Explain that this code is ok. Otherwise, the error must be handled manually.

This may lead to a serious consequence: all function calls need to be written as

if err := function(); err != nil

such a structure. Such consequences are almost catastrophic (this is why after VS2022 supports the code AI completion function, the hot reviews on the Internet are all positive for Gopher). This error mechanism is also the worst place for Go to be hacked.

At this time, some friends must have said, so what if I don’t deal with it and create a code similar to 1/0?

If you write code similar to the above, it will eventually cause a Go panic. In my current shallow understanding, panic is actually the concept of Exception in C#, because the program will crash completely after encountering a panic. Go designers probably thought that all errors should be used in the initial design. Error processing, if a panic is triggered, it means that the program cannot be used. Therefore, panic is actually an irreparable wrong concept.

However, in a large-scale project, it is not that the code written by oneself is foolproof and there will be no panic. It is very likely that other packages we refer to do something that we do not know and then panic. For example, the most typical example: Go's httpRequest The Body can only be read once, and it will be gone after reading. If the web framework we use reads the Body when processing the request, we may panic when we read the result again.

Therefore, in order to solve the panic, Go also has a recover() function, the general usage is:

func main() {
    
      
 panic(1)  
 defer func() {
    
      
 if err := recover(); err != nil {
    
      
 fmt.Println("boom")  
 }  
 }  
}

In fact, Go has a strong competitor—Rust. Rust is a language developed by the Mozilla Foundation in 2010. Similar to Go, which is developed based on C language, Rust is developed based on C++. So now there are two camps in the community, Go and Rust, arguing with each other and chattering endlessly. Of course, there is no silver bullet for everything, and everything should be learned and understood with dialectical thinking.

Well, after reading the Go grammar above, I will definitely write some Go code exercises to deepen my memory. Just right, let's implement a small example on the Go official website, and implement this interface for calculating the Nth number of the Fibonacci sequence by yourself.

type Fib interface {
    
      
 // CalculateFibN 计算斐波那契数列中第N个数的值  
 // 斐波那契数列为:前两项都是1,从第三项开始,每一项的值都是前两项的和  
 // 例如:1 1 2 3 5 8 13 ...  
 CalculateFibN(n int) int  
 }

Due to space limitations, I put the answer to this small exercise in my gitee, and welcome everyone to visit.

Extension link:

What are the three necessary capabilities that a front-end reporting tool needs to have?

Real-time Data Visualization - Edge Analytics Scenarios for Modern Data

Java Batch Operation Excel File Practice

Guess you like

Origin blog.csdn.net/powertoolsteam/article/details/131665242