Go Study Bible: 0 Basics Proficient in GO Development and High Concurrency Architecture (1)

GO Study Bible: Underlying Principles and Practice

Say up front:

It is very difficult to get an offer now , and I can't even get a call for an interview.

In Nien's technical community (50+), many small partners have obtained offers with their unique skills of "left-handed cloud native + right-handed big data", and they are very high-quality offers. It is said that the year-end awards are all for 18 months .

From the perspective of Java high-paying positions and employment positions, cloud native, K8S, and GO are now becoming more and more important for senior engineers/architects. From the perspective of an architect, Nien wrote a "GO Study Bible" based on his knowledge system and knowledge universe of Nien 3 high-level architects

final learning objective

Our goal is not only the freedom of GO application programming, but also the freedom of GO architecture.

Some time ago, a 2-year buddy wanted to raise his salary to 18K. Nien wrote the project structure of the GO language into his resume, which made his resume shiny and reborn, and he could get 30K from Toutiao and Tencent. With the offer, the annual salary can directly increase by 20W .

It is enough to explain the gold content of the GO architecture.

In addition, Nine's cloud native did not involve GO, but cloud native without GO is incomplete.

Therefore, after learning the GO language and GO architecture, let's go back and complete the second part of cloud native: "Istio + K8S CRD architecture and development practice" to help everyone thoroughly penetrate cloud native.

This article directory

Article directory

Related materials for learning GO:

The Go Programming Language (google.cn)

Dubbo Java 3.1.4 officially released | Apache Dubbo

dubbo-go

Install Dubbo-go development environment | Apache Dubbo

Complete an RPC call | Apache Dubbo

Comparison of the four major languages ​​of Go/C/C++/Java

Process Oriented C Language

The background of C language mainly has the following aspects:

  1. Software Crisis: In the 1960s, as computer software scaled up, software design and development faced enormous challenges. Many software projects are slow, expensive, or fail completely. To solve this problem, a more efficient and reliable programming language is needed.
  2. Disadvantages of ALGOL language: ALGOL language is one of the most popular programming languages ​​in the 1960s, but its expressive power and readability are not strong enough. Therefore, a new programming language is needed, which can not only retain the advantages of ALGOL, but also enhance expressiveness and readability.
  3. The emergence of the UNIX operating system: In the early 1970s, Bell Labs developed the UNIX operating system, and the core part of the operating system was written in C language. Due to the high efficiency and flexibility of the C language, it has gradually become the preferred development language for UNIX operating systems and other system software.

Based on the above reasons, Dennis Ritchie started to develop the C language in the early 1970s, and officially released the first stable version in 1972. The C language has the characteristics of high efficiency, strong expressive ability, and good portability, and has become a very important one among general-purpose programming languages. At present, C language is widely used in operating systems, embedded systems, programming language interpreters, etc., and is also the basis of many programming languages.

In the 1970s, a very important language was born, which is today's famous C language. The father of the C language is a well-known computer expert in the United States. Dennis Leach.

C language is currently one of the most commonly used programming languages ​​in the world.

The father of C language: Dennis Ritchie (Dennis Ritchie). Famous American computer expert, inventor of C language, father of UNIX. Invented the C language and the Unix operating system during 1969-1973.

Object-oriented C++ language

In 1982, Dr. Bjarne Stroustrup of Bell Laboratories in the United States introduced and expanded the concept of object-oriented on the basis of C language, and invented a new programming language. In order to express the relationship between the language and the C language, it was named C++.

The background of the C++ language mainly has the following aspects:

  1. Problems with C language: C language is an efficient and flexible programming language, but it has some problems in terms of type safety, modularity and code reuse. Therefore, Bjarne Stroustrup hopes to develop a new programming language to solve these problems while retaining the advantages of C language.
  2. Rise of Object-Oriented Programming: In the 1980s, Object-Oriented Programming began to flourish, with languages ​​like Smalltalk, Simula, and others leading the way. Bjarne Stroustrup wanted to develop a new programming language that would enable object-oriented programming.
  3. Improvements in hardware performance: In the late 1980s and early 1990s, computer hardware performance improved dramatically, allowing programmers to write programs in more complex programming languages. Therefore, Bjarne Stroustrup wanted to develop a new programming language that could meet this need.

Based on the above reasons, Bjarne Stroustrup started to develop the C++ language in 1983, and officially released the first stable version in 1985. The C++ language combines the efficiency of the C language and the characteristics of object-oriented programming, and has become a very important one in general-purpose programming languages.

The C++ language is widely used in game development, operating systems, large-scale software systems and other fields, and is also the basis of many programming languages.

The father of C++: Bjarne Stroustrup (Benjani Stroustrup).

Java language

Java was officially released in May 1995. The emergence of java coincided with the vigorous rise of the Internet, and because of the characteristics of the language, java has great advantages on the Internet, and its release was extremely popular at the beginning.

The background of the Java language mainly has the following aspects:

  1. The rise of object-oriented programming: In the late 1980s and early 1990s, object-oriented programming gradually became the mainstream programming paradigm, and languages ​​such as C++ received widespread attention. Sun Microsystems hopes to develop a new, more advanced object-oriented programming language.
  2. Cross-platform requirements: In the early 1990s, computer hardware and operating system environments were relatively complex, and software needed to be written and debugged for different platforms. Sun Microsystems hopes to develop a new programming language that can be written and run across platforms.
  3. The advent of the Internet: The advent of the Internet led to the pursuit of large-scale distributed application development. Sun Microsystems wanted to develop a new programming language that would make it easy to build distributed network applications.

Based on the above reasons, Sun Microsystems launched the Oak project (later renamed Java) in 1991, and officially released the first stable version in 1995. The Java language was originally designed for home appliance controllers, but with the development of the Internet, the Java language quickly became the preferred development language for network applications. At present, the Java language has become the mainstream of enterprise-level application development, and is widely used in many fields such as finance, e-commerce, and big data.

The original model of java was developed in 1991 by its founder James Gosling. At that time, it was still called Oak. Later, James hoped that using the Java language could be as easy and enjoyable as drinking coffee. Renamed to java.

The father of Java is James Gosling (James Gosling).

Golan:

The Go language, also known as Golang, is a statically typed, compiled, and highly concurrent programming language developed by Google.

It was originally designed by Robert Griesemer, Rob Pike, and Ken Thompson in 2007, and the first stable version was officially released in 2009.

The background of the Go language mainly has the following aspects:

  1. Concurrent programming requirements: With the improvement of computer performance and the popularity of Internet applications, more and more applications require high concurrent processing capabilities. However, traditional programming languages ​​such as C++ and Java have many problems in concurrent programming. Therefore, Google hopes to develop a new programming language that can better meet this demand.
  2. Programmer productivity: Google has a large number of programmers and code bases, while traditional programming languages ​​are complex and have a relatively large amount of code, resulting in low programmer productivity. Therefore, Google hopes to develop a programming language that is concise, easy to learn and use.
  3. Code security: When writing large-scale software systems, security issues such as buffer overflows often occur. Therefore, Google hopes to develop a new programming language that can avoid these problems.

Based on the above reasons, Google started the development of Go language in 2007.

Rob Pike, one of the designers of the Go language, said that the goal of the Go language is to "make programming more enjoyable and more productive."

After several years of development and improvement, the Go language has gradually become widely used and has become the language of choice in some fields.

The Go language focuses on simplicity and performance in design, and the goal is to provide an efficient, reliable, easy-to-write and maintain system-level programming language. It has the following characteristics:

  1. Easy to learn: Go language has a simple syntax and only 25 keywords, making it easy to learn and use.
  2. Strong concurrency: Go language natively supports coroutines and channels, which can easily implement high-concurrency programs.
  3. Memory management: Go language has automatic memory management and garbage collection mechanism to avoid common memory errors.
  4. High performance: The compilation speed of Go language is fast, and the generated binary file is small in size and fast in operation speed.
  5. Cross-platform support: Go language can be compiled and executed on multiple operating systems and hardware platforms.
  6. Natural network support: The Go language standard library contains a wealth of network programming interfaces, which can easily implement web applications.

In short, the Go language is a programming language oriented to modern programming needs. Its simplicity, high performance, and strong concurrency make it widely used in the fields of the Internet, big data, and network programming. At present, the Go language has become one of the languages ​​of choice for many developers.

The three authors of Go are: Rob Pike (Rob Parker), Ken Thompson (Ken Thompson) and Robert Griesemer (Robert Griesemer)

Comparison of Golang and other languages ​​​​such as Java

Golang, Java, and C are all common programming languages ​​that have the following relationship:

  1. Golang is a programming language developed by Google, which was originally used to meet the needs of large-scale network applications. Java is also a language invented by Sun Microsystems (now Oracle) and is currently widely used in the development of enterprise-level applications.
  2. Both Java and C are traditional programming languages ​​with a long history and wide application fields. In contrast, Golang is a relatively young language, but its rich standard library and superior performance give it advantages in certain scenarios.
  3. Both Java and Golang have good cross-platform support. Java uses the Java Virtual Machine (JVM) to achieve cross-platform, while Golang uses its own compiler to achieve cross-platform. Although C can also achieve cross-platform, it needs to manually write platform-related code, and it is not as convenient as Java and Golang.
  4. In the field of use, Java is mainly used to build large-scale enterprise applications, such as e-commerce websites, financial systems, etc.; Golang is mainly used to build Internet applications, distributed systems and network servers, etc.; and C is mainly used for operating systems, embedded systems, drivers, etc.
  5. In terms of language features, both Java and Golang are strongly typed languages ​​with good code readability and maintainability. C is a relatively low-level programming language that requires manual management of memory and other resources.

In short, Golang, Java, and C are all different programming languages, with differences in features, application scenarios, and language ecology. When choosing which programming language to use, it needs to be evaluated and selected according to the actual application needs and personal skill level.

Go borrows many language features from its three ancestors: C, Pascal, and CSP.

Go's syntax, data types, control flow, etc. are inherited from C. Go's package and object-oriented ideas come from the Pascal branch. The biggest language feature of Go, the coroutine concurrency model based on pipeline communication, is borrowed from the CSP branch.

Java

Compiled language, moderate speed (2.67s), the current large-scale websites are written in java, such as Taobao, JD.com, etc.

The main features are stability, good open source, its own set of writing specifications, moderate development efficiency, and currently the most mainstream language.

As the biggest names in programming languages. Has the greatest popularity and user base. Regardless of the storm, I will not move. He is strong and he is strong, and the breeze blows the hills;

c#

Fast execution (4.28), moderate learning difficulty, and moderate development speed. However, due to the many shortcomings of c#, the predecessors of large websites such as JD.com and Ctrip were all developed in c#, but now they are all migrated to java.

C/C++

The ancestor of existing programming languages, from which other languages ​​​​are born. Execution speed is unmatched. But it is the most complicated to write and difficult to develop.

Javascript

A maverick tsundere beauty in programming languages. The front-end processing capability is unmatched by other languages. The developing js backend processing capability is also remarkable. Front-end and back-end take all, which one is better than me?

Python

Scripting language, the slowest (258s), the code is concise, the learning progress is short, and the development speed is fast. Douban is written in python. Python's famous server frameworks include django and flask. However, python is not stable on large-scale projects, so some enterprises that use python later migrated to java.

scala

The compiled language is ten times faster than python, similar to java, but the learning progress is slow, and in actual programming, if you are not proficient in the language, it is easy to cause serious performance degradation. , Later, for example, Yammer migrated from scala to java. Microservice frameworks include lagom, etc.

Go

The little fresh meat of the programming world. The high concurrency capability is unmatched. That is, it has the same concise code and development speed as Python, and has the same execution efficiency as C language, with outstanding advantages.

Official website of Go language

The Go language was designed to solve the problems encountered by Google development at that time:

  • A lot of C++ code, while introducing Java and Python
  • thousands of engineers
  • tens of thousands of lines of code
  • distributed compilation system
  • millions of servers

Pain points in Google development:

  • compile slow
  • runaway dependence
  • Each engineer only uses a part of a language
  • Programs are difficult to maintain (poor readability, unclear documentation, etc.)
  • Updates take longer and longer
  • Difficulty cross compiling

How to solve Google's problems and pain points?

Go wants to be the C language of the Internet age. The fundamental programming philosophy of most system-level languages ​​(including Java and C#) comes from C++, which further develops the object-oriented nature of C++. But the designers of the Go language have a different view. They think that the C language is worth learning. The enduring root of the C language is that it is simple enough. Therefore, the Go language is also simple enough.

Therefore, their goal in designing Go was to eliminate all kinds of slowness and cumbersomeness, and to improve various inefficiencies and scalability. Go was designed by and for those who develop large systems; it is to solve engineering problems, not to study language design; it is to make our programming more comfortable and convenient.

However, combined with some realities within Google at the time, for example, many engineers were from the C department, so the newly designed language must be easy to learn, preferably a language similar to C; no new language has been released in 20 years, so the newly designed language must be easy to learn. The language has to be modern (e.g. built-in GC), etc. Finally, based on actual combat experience, they designed the Go language towards the goal.

Go language syntax is simple, including C-like syntax. Because the Go language is easy to learn, an average college student can write a hands-on, high-performance application in a few weeks. Everyone in China pursues speed, which is one of the reasons why Go is popular in China.

The reason why Go is called Go is to express that the running speed, development speed and learning speed of this language are as fast as gopher.

Gopher is a small animal that lives in Canada. Go's mascot is this small animal. Its Chinese name is gopher. Their biggest feature is that they dig holes very fast, and of course they may not only dig holes.

tuboshu

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

The main development process of Go language

The Go language is the second open source programming language released by Google in 2009.

The Go language is specially optimized for the programming of multi-processor system applications. Programs compiled with Go can match the speed of C or C++ codes, and are safer and support parallel processes.

  • September 2007, prototype design, Rob Pike (Rob. Parker) officially named Go;
  • In May 2008, Google fully supported the project;
  • On November 10, 2009, the first public release, Go open source all the code, it won the language of the year;
  • On March 16, 2011, the first stable version r56 of the Go language was released.
  • On March 28, 2012, the first official version of the Go language, Go1, was released.
  • On April 04, 2013, the first Go 1.1beta1 test version of the Go language was released.
  • On April 08, 2013, the second Go 1.1beta2 beta version of the Go language was released.
  • On May 02, 2013, the Go language Go 1.1RC1 version was released.
  • On May 07, 2013, the Go language Go 1.1RC2 version was released.
  • On May 09, 2013, the Go language Go 1.1RC3 version was released.
  • On May 13, 2013, the official version of Go language Go 1.1 was released.
  • On September 20, 2013, the Go language Go 1.2RC1 version was released.
  • On December 1, 2013, the official version of Go language Go 1.2 was released.
  • On June 18, 2014, the Go language Go version 1.3 was released.
  • On December 10, 2014, the Go language Go version 1.4 was released.
  • On August 19, 2015, the Go language Go version 1.5 was released, and the "last remaining C code" was removed in this update.
  • On February 17, 2016, the Go language Go version 1.6 was released.
  • On August 15, 2016, the Go language Go version 1.7 was released.
  • On February 17, 2017, the Go language Go version 1.8 was released.
  • On August 24, 2017, the Go language Go version 1.9 was released.
  • On February 16, 2018, the Go language Go version 1.10 was released.
  • On August 24, 2018, the Go language Go version 1.11 was released.
  • On February 25, 2019, version 1.12 of the GO language was released.

The Go language originated in 2007 and was officially released in 2009. It started as a 20% part-time project of Google from September 21, 2009, that is, relevant employees use 20% of their spare time to participate in the research and development of Go language.

In fact, it can be seen that the history of the Go language is not very short.

In November 2009, the first version of the GO language was released. In March 2012, the first official version Go1.0 was released.

In August 2015 go1.5 was released and this version is considered historic. Completely remove the C language part, use GO to compile GO, and a small amount of code is implemented using assembly. In addition, they invited Rick Hudson, an authoritative expert in memory management, to redesign GC, support concurrent GC, and solve the problem of GC time delay (STW), which has been widely criticized for a long time. And in subsequent versions, GC has been further optimized. By the time of go1.8, the GC delay in the same business scenario can be controlled within 1ms from a few seconds in go1.1. To solve the GC problem, it can be said that the GO language has almost smoothed out all the weaknesses in server-side development.

Until February 25 this year, the latest version of the Go language was Go 1.12.

During the version iteration process of the GO language, the language features have basically not changed much, basically maintaining the benchmark of GO1.1, and the official promise is that the new version is fully compatible with the code developed under the old version. In fact, the GO development team is very cautious in adding new language features, and has carried out continuous optimization in terms of stability, compilation speed, execution efficiency, and GC performance.

Features of the Go language

The Go language has the following characteristics:

  1. High concurrency: Go language natively supports coroutines and channels, which can easily implement high-concurrency programs and perform well in multi-core, distributed and other scenarios.
  2. Memory safety: Go language avoids common memory errors (such as buffer overflow, wild pointer, etc.) through garbage collection mechanism and pointer restriction, and improves the reliability of the program.
  3. Fast compilation: The compilation speed of Go language is very fast, and there will be no obvious compilation delay even in large projects. At the same time, the executable files generated by the Go language are small in size and fast in operation.
  4. Easy to learn: Go language has simple syntax and clear structure, making it easy to learn and use. Its standard library design is also very simple, making it easy for developers to get started.
  5. Cross-platform support: Go language can be compiled and executed on multiple operating systems and hardware platforms, and has good cross-platform support performance.
  6. Rich tool chain: The Go language provides a series of tools, such as go fmt, go vet, go test, etc., to help developers more easily perform code formatting, inspection, and testing.
  7. Natural support for network programming: the Go language standard library contains a wealth of network programming interfaces, which can easily implement web applications and distributed systems.

In short, the Go language is a simple, efficient, safe, easy-to-learn, and highly concurrent programming language. It is favored for its many features and has been widely used in the fields of Internet, big data, network programming and so on.

To put it simply, the Go language is a language that has both execution efficiency and development efficiency.

go_logo

Before the emergence of go, neither assembly language nor dynamic scripting language could have both execution efficiency and development efficiency.

Execution efficiency execution speed: C/C++ > Java > PHP
development efficiency developing efficiency: PHP > Java > C/C++

Core features of the Go language

The reason why the Go language is powerful is that it can always grasp the pain points of programmers in the development of the server and solve problems in the most direct, simple, efficient and stable way.

The core features of the Go language include the following:

1 concurrency:

The Go language is inherently capable of high concurrency. Through goroutine (lightweight thread) and channel (channel), concurrent programming can be easily realized, avoiding some problems in multi-threaded programming.

2 Memory recovery (GC)

From C to C++, from the perspective of program performance, these two languages ​​​​allow programmers to manage memory by themselves, including memory application and release. Because there is no garbage collection mechanism, C/C++ runs very fast, but with it comes the programmer's very careful consideration of memory usage. Because even a little carelessness may cause "memory leaks" to waste resources or "wild pointers" to cause program crashes, etc. Although C++11 later used the concept of smart pointers, programmers still need to use them carefully. Later, in order to improve the speed of program development and the robustness of programs, high-level languages ​​such as java and C# introduced GC mechanisms, that is, programmers no longer need to consider memory recovery, but language features provide garbage collectors to reclaim memory. But what follows may be a reduction in the efficiency of the program.

The GC process is: stop the world first, scan all objects to judge them alive, mark the recyclable objects in a bitmap area, then start the world immediately, restore the service, and at the same time start a special goroutine to reclaim the memory and put it in the free list for recovery Used, not physically released. Physical freeing is performed periodically by a dedicated thread.

The bottleneck of GC is that all objects must be scanned every time to judge activity. The more objects to be collected, the slower the speed.

An empirical value is that it takes 1ms to scan 100,000 objects, so try to use a solution with fewer objects. For example, we consider linked list, map, slice, and array for storage. Each element of linked list and map is an object, while slice or array is an object, so a slice or array is good for GC.

GC performance may be continuously optimized as the version is continuously updated. This area has not been carefully investigated. There are HotSpot developers in the team, and they should learn from the design ideas of jvm gc, such as generational recycling and safepoint.

  • Memory is automatically reclaimed, and developers are no longer required to manage memory
  • Developers focus on business implementation, reducing mental burden
  • Only need new to allocate memory, no need to release

3 memory allocation

In the initialization phase, a large memory area is directly allocated. The large memory is divided into blocks of various sizes and put into different free lists. When the object allocates space, a memory block of an appropriate size is taken from the free list.

When memory is reclaimed, unused memory will be put back into the free list.

Free memory will be merged according to a certain strategy to reduce fragmentation.

4 Quick compilation:

The compilation speed of Go language is relatively fast, and there will be no obvious compilation delay in large projects. At the same time, the executable files generated by the Go language are small in size and fast in operation.

5 Easy to learn

Go language has simple syntax and clear structure, making it easy to learn and use. Its standard library design is also very simple, making it easy for developers to get started.

6 Cross-platform support:

The Go language can be compiled and executed on multiple operating systems and hardware platforms, and has good cross-platform support performance.

7 Rich tool chain:

The Go language provides a series of tools, such as go fmt, go vet, go test, etc., to help developers more easily perform code formatting, inspection, and testing.

8 supports network programming naturally:

The Go language standard library contains a wealth of network programming interfaces, which can easily implement web applications and distributed systems.

In short, the Go language has core features such as high concurrency, memory safety, fast compilation, easy learning, cross-platform support, and rich tool chains, making it a very popular programming language. field has been widely used.

Go language industry case

The Go language is widely used in many industries. The following are some representative industry cases:

  1. Internet: Google, Zhihu and many other Internet companies are using the Go language for background development. For example, Google's web crawler system Google Crawler is written in Go language.
  2. Finance: Many companies in the financial field have also begun to use the Go language for development. For example, Stripe, an American online payment company, has written a high-performance data analysis system in Go language.
  3. Games: The Go language is also widely used in the game industry. For example, Tencent's game server framework TarsGO is written in Go language.
  4. Big data: The Go language has the characteristics of high concurrency and high performance, so it is also popular in the field of big data. For example, Uber’s Cherami messaging system is written in Go.
  5. Blockchain: The Go language has also become one of the preferred programming languages ​​in the blockchain field. For example, Geth, the Ethereum client, is written in Go.

In conclusion, the Go language has been widely used in many industries, and as it is continuously improved and optimized, its scope of use will be further expanded.

Except for the famous Docker, it is completely implemented with GO. The industry's most popular container orchestration management system, kubernetes, is completely implemented in GO. Afterwards, Docker Swarm is completely implemented with GO.

In addition, there are various well-known projects, such as etcd/consul/flannel, Qiniu cloud storage, etc., which are all implemented using GO.

Some people say that the reason why the GO language is famous is that it has caught up with the cloud era.

But why not put it another way? It is also the GO language that promotes the development of the cloud.

In addition to cloud projects, there are also companies like Toutiao and Uber who have also used the GO language to completely restructure their business.

Relaxed working environment created by GO language

The generation of the go language benefits from Google's relaxed technical research environment.

Google's "20% time" working method allows engineers to spend 20% of their time on research projects they like.

The voice service Google Now, Google News, Google News, and traffic information on Google Maps are all products of 20% of the time.

The Go language was originally a product of 20% of the time.

This point is especially worth learning from major domestic manufacturers. The 996 is popular among major domestic manufacturers, which squeezes everyone's working hours to the extreme, strictly controls everyone's technological output, and everyone has no room for innovation and creativity.

The management methods of major domestic manufacturers are extremely unfavorable to technological innovation. You will not be able to enjoy the winner-take-all bonus brought about by technological innovation and patent innovation.

Go language development environment construction

Most used link

Go language SDK tools include the following:

  1. Go Compiler:
    A Go compiler is a tool that compiles Go source code into an executable. The Go compiler can be used from the command line or from an integrated development environment (IDE).
  2. Go development environment (IDE):
    There is no officially recommended IDE for the Go language, but there are many third-party IDEs that developers can use.
    For example, GoLand, Visual Studio Code, Sublime Text, etc.
  3. Go fmt:
    Go fmt is a tool for formatting code, which can make Go code more standardized and easy to read.
  4. Go vet:
    Go vet is a static code analysis tool that can check for potential problems in the code and provide corresponding suggestions.
  5. Go test:
    Go test is a unit testing tool that helps developers write test cases and perform automated testing.

In short, the development tools of the Go language are rich and diverse, and you can choose the most suitable tool for development according to your personal needs.

Installation of Go Compiler SDK

Golang is a programming language, its installation and usage steps are as follows:

Install Golang:

Download the installation file for the corresponding operating system from the official website.

All releases - The Go Programming Language (google.cn)

It's in Nin's kit, ready for everyone

Golang's official website provides multiple versions of binary installers, and you can choose the corresponding version according to different operating systems and hardware architectures.

  • For Windows systems, the installer can be downloaded as a .msi file,
  • For Linux or Mac OS X systems, a tarball of the source code or binaries can be downloaded.

Windows system, use the installation program of the .msi file, follow the wizard all the way, and the installation is ok.

Golang installation path

Configure environment variables:

After the installation is complete, we need to add the Golang installation path to the system environment variables.

For Windows system, you can right click on "Computer" or "My Computer" and select "Properties", click "Environment Variables" in "Advanced System Settings", and add environment variables "GO_HOME" and "GOPATH" in "System Variables" , pointing to the installation path and working directory of Golang respectively.

GO_HOME  指向  Golang的安装路径
GOPATH   指向  工作目录

The GO_HOME environment variable is as follows:

On the Windows system, you can open the "Control Panel" -> "System and Security" -> "System", click "Advanced System Settings", then click the "Environment Variables" button, find the "Path" variable in the "System Variables" , add the Golang bin directory path, for example "C:\Go\bin".

C:\Program Files\Go\bin has automatically entered the path environment variable, no need to add it manually:

For Linux or Mac OS X systems, the Golang installation path can be added to the PATH environment variable, for example, in the ~/.bashrc file:

export  PATH=$PATH:/usr/local/go/bin

The above are the installation and usage steps of Golang. Golang has the advantages of high efficiency, simplicity, and security, and is suitable for building various types of applications, such as servers, network services, and command-line tools.

Comparison between GoLang SDK installation and Java SDK installation

It's exactly the same, just the name is different.

Golang integrated development tool IDE

The following are some commonly used Golang development tools:

  1. GoLand: Developed by JetBrains, it is an integrated development environment (IDE) with rich features and plug-in support.
  2. Visual Studio Code: A lightweight code editor that uses Go extensions to get similar functionality to GoLand.
  3. Sublime Text: Another popular code editor, there are also many plugins available to support Golang development.
  4. Vim: A powerful text editor that supports Golang development by adding plugins and configurations.
  5. LiteIDE: A development environment specially created for Golang, which supports basic functions such as auto-completion, suitable for beginners.
  6. Eclipse: A widely used IDE that can support Golang development by installing the corresponding plug-ins.

The above are some commonly used Golang development tools, you can choose the most suitable development tool for you according to your personal preference.

GoLand installation and use

Most of the friends are very familiar with the idea of ​​JetBrains, and the GoLand provided by this company is recommended here.

GoLand is an integrated development environment (IDE) developed by JetBrains for the Go programming language.

The following are the installation and usage steps of GoLand:

Download the installation package:

Download the installation file for the corresponding operating system from the official website https://www.jetbrains.com/go/download/. It's in Nin's kit, ready for everyone

Install GoLand:

Run the installation package and follow the prompts to install.

Open GoLand:

After opening GoLand, you will see a welcome screen. Here you can create a new project, open an existing project, etc.

Create project:

Select "Create New Project" on the welcome screen, select the project type and storage location, and click the "Create" button.

create a project

Configure the Go SDK:

Open the File -> Settings menu, select "Go" in the left navigation bar, and then add your Go SDK path in the right "GOROOT" option, such as "/usr/local/go".

When done, the empty project looks like this:

Write code:

Write code in the editor, create a new file named hello, type simple application, as follows:

The new go file is created, as follows:

Write a hello world case:

There are two sentences in total:

  • One sentence is import "fmt"
  • One sentence is fmt.Println("Hello, World!")

What does the first sentence mean? import "fmt"is the statement used to import the standard fmtlibrary .

fmtProvides functions for formatting input and output, such as: Println()and Printf()etc. , which can print the output content on the console.

This package also includes some other functions, such as string formatting and parsing, file reading and writing, etc.

Use import "fmt"allows you to use the functions in the fmt package in Go programs to facilitate input and output operations.

For example the code above:

import "fmt"

func main() {
    
    
    fmt.Println("Hello, World!")
}

In this code, we import "fmt"import fmtthe package using .

What does the second sentence fmt.Println("Hello, World!") mean?

fmt.Println("Hello, World!")is a statement in the Go language used to output "Hello, World!" to the console.

Println fmtis Println()a function located in the package.

In this example, we use fmt.Println()the function to print the given arguments in order, adding a newline after the last argument.

Therefore, passing it "Hello, World!"as an argument to fmt.Println()the function will display on the console Hello, World!with a newline.

main()The function calls Println()the function to output Hello, World!to the console.

Run the program:

Click the run button (green triangle) at the top of the editor or use the shortcut key Shift+F10 to run the program.

The result of the operation is as follows:

The above are the installation and usage steps of GoLand.

GoLand has a good user interface, rich functions and plug-in support, which can improve development efficiency and make Go language development easier and more efficient.

How Go Executes and Go Commands

Go's source code file classification:

In Go, source code files are divided into 4 types:

  1. Command source code file:
  • .gofiles with the extension
  • The command source file belongs to the main package
  • Contains the main method, which is the entry point of the program and must be owned by every program that can run independently.
  1. Library source code file:
  • .goFiles with the extension .
  • Does not contain the main entry method.
  1. Test file:
  • _test.goSource code files with the suffix
  • Test files are used to write test code such as unit tests and performance tests.
  1. C language source code file:

Files with suffixes such as .c, or.h are used to contain some auxiliary codes or tools implemented in C language..s

  1. No source files:
  • Files that do not contain code, such as README, LICENSE, etc.
  • Although these files have no effect on the running of the program, they are of great significance to developers.

Among them, .gosource code files and _test.gotest files are the most common file types.

Overall:

  • In a Go project, all source code files are usually organized in one or more packages, and corresponding interfaces are provided externally. Test files are used to test the correctness and performance of these interfaces.
  • In addition, Go also supports nested package and vendor directories, making the organizational structure of the project more flexible and clear.
  • At the same time, when Go compiles, related binary files, static libraries or dynamic link libraries and other files will be automatically generated.

1. Command source code file:

Two characteristics of command source files:

  • Declare that you belong to the main code package,
  • Contains the main function, which is a function named main with no parameter declaration and no result declaration.

The role of the command source file:

  • The command source file is the entry point for a Go program.
  • Command source code files can be run independently.

You can use the go run command to run it directly, or you can get the corresponding executable file through the go build or go install command. So the command source code file can be run in any directory of the machine.

step1: Directly run the command source code file through the go run command:

It is best not to put multiple command source code files in the same code package. Although multiple command source code files can be run separately by go run, they cannot be run through go build and go install.

After the command source file is installed by go install, it becomes an exe executable file. Where does it usually go?

  • If GOPATH has only one workspace, the corresponding executable file will be stored in the bin folder of the current workspace;
  • If there are multiple workspaces, they will be installed in the directory pointed to by GOBIN.

Copy hello.go to hello2.go file, and modify the content inside:

There are two go files in the hello directory, and two command source files:

  • One is hello.go
  • One is hello2.go.

Let me explain first, two command source code files are placed in the above folder, and both declare that they belong to the main code package: package main .

Open the console terminal, enter the work directory, execute the go run command to execute the two command source files, and you can see that both go files can be executed:

There is no problem with the execution of go run above, but the problem of executing go build and go install comes.

See what happens:

PS C:\Users\nien\go\src\awesomeProject> go build 
# awesomeProject
.\hello2.go:5:6: main redeclared in this block
        .\hello.go:5:6: other declaration of main
PS C:\Users\nien\go\src\awesomeProject>  go install
# awesomeProject
.\hello2.go:5:6: main redeclared in this block
        .\hello.go:5:6: other declaration of main

Running effect diagram:

This also proves a problem: although multiple command source code files can be run separately by go run, they cannot pass go build and go install.

2. Library source code files

Library source code files are source code files that do not have the above two characteristics of command source code files. Ordinary source code files that exist in a code package.

In the Go language, library source code files also need to follow certain naming rules and writing specifications.

Here are some general concepts and steps:

  1. Create a new package, the package name should be the same as the folder name.
  2. Write code in a package, which can contain multiple source code files.
  3. To expose functions, types or variables that need to be accessed by other packages, you need to add capital letters in front of their names.
func Add(a, b int) int {
    
    
   return a + b
}
  1. To write documentation comments, you can use go docthe command to view documentation.
// Add 将两个整数相加并返回结果
func Add(a, b int) int {
    
    
    return a + b
}
  1. Use the go buildor go installcommand to build and install the library. After installation, it can be imported and used in other programs.
$ go install github.com/xxx/yyy

The above are the basic flow and examples of library source code files. For more details and advanced usage, please refer to the official documentation:

How to Write Go Code - The Go Programming Language (google.cn)

After the library source code files are installed, the corresponding archive files (.a files) will be stored in the platform-related directory of pkg in the current workspace.

3. Test source code files

In the Go language, test source code files need to follow certain naming rules and writing specifications.

Here are some general concepts and steps:

  1. Create a file xxx_test.gonamed , where xxxis the name of the source file to test.
  2. Import testingthe package .
import "testing"
  1. Write a test function with a function name Teststarting with and a function signature of func TestXxx(*testing.T), where Xxxis the name of the function to test.
func TestAdd(t *testing.T) {
    
    
    // 测试代码...
}
  1. In the test function t.Error, t.Failmethods such as or can be used to judge whether the test result meets expectations.
func TestAdd(t *testing.T) {
    
    
    if add(1, 2) != 3 {
    
    
        t.Errorf("add function failed")
    }
}
  1. To run the test program, you can use go testthe command to run all test files in the current directory.
$ go test
PASS
ok      command-line-arguments  0.001s

For more details and advanced usage, please refer to the official documentation: https://golang.google.cn/pkg/testing/

In addition, code files with the suffix of _test.go must contain functions prefixed with the name of Test or Benchmark:

  • TestXXX are functional test functions
  • BenchmarkXXX performance test function

details as follows:

func TestXXX( t *testing.T) {
    
    
}

A function whose name is prefixed with Test can only accept parameters of *testing.T, and this kind of testing function is a functional testing function .

func BenchmarkXXX( b *testing.B) {
    
    
}

Functions whose names are prefixed with Benchmark can only accept parameters of *testing.B, and this kind of testing function is a performance testing function .

Main Go commands

At present, the latest version of Go 1.12 contains the following 17 basic commands.

We can open the terminal and enter: go help to see these commands and introductions of Go.

Among them, there are four related to compilation: build, get, install, and run. Next, let’s take a look at the functions of these four in turn.

Before analyzing these 4 commands in detail, let's list the common command flags. The following commands are applicable:

name illustrate
-a Used to force a recompilation of all involved Go packages (including those in the Go standard library), even if they are already up to date. This flag gives us the opportunity to do some experimentation by changing the underlying code package.
-n Causes the command to just print all the commands used in its execution without actually executing them. It's fine to use if you don't just want to view or verify the execution of commands, but don't want to change anything.
-race Used to detect and report data race problems in the specified Go language program. This is one of the most important checks when writing concurrent programs in Go.
-v Used to print the code packages involved in the execution of the command. This always includes the target code package we specify, and sometimes those code packages that the code package depends on directly or indirectly. This will let you know which code packages were executed.
-work It is used to print the name of the temporary working directory generated and used when the command is executed, and it is not deleted after the command is executed. The files in this directory may be useful to you, and you can also understand the execution process of the command from the side. If this flag is not added, the temporary working directory will be deleted before the command completes.
-x Causes the command to print all commands used during its execution and execute them simultaneously.

1、go run

go runA command is a built-in command in the Go language to compile and run one or more Go source code files.

Its function is roughly similar to the combination of javac + java in the Java language, that is, compile first and then run.

It can run Go programs directly without generating executable files. For some small projects or temporary tests, the compilation process can be omitted.

Note that this command is not used to run all Go source files! The go run command can only accept one command source code file and several library source code files (must belong to the main package) as file parameters, and cannot accept test source code files .

It checks the type of the source code file when it is executed. If there are multiple or no command source files in the parameter, then the go run command will only print an error message and exit without continuing to execute.

The following describes the detailed usage of go runthe command :

  1. There is only one source file
    If there is only one source file, it is very simple to use go runthe command , just specify the path of the source file to run the program:
$ go run hello.go
  1. Multiple source files
    If there are multiple source files, you can list all source files and compile and run according to dependencies:
$ go run file1.go file2.go file3.go

Or use wildcards to match all matching source files:

$ go run *.go
  1. Passing parameters
    We can pass parameters to the program by adding parameters after go runthe command . These parameters are saved in os.Argsthe slice and can be accessed programmatically. For example:
$ go run hello.go arg1 arg2 arg3
  1. Interactive input
    If you need to read user input from standard input, you can use functions fmt.Scansuch as to read input:
package main

import "fmt"

func main() {
    
    
    fmt.Println("Hello, World!")

    var input string
    fmt.Scanln(&input)

    fmt.Println("你的输入是:%s", input)

}

Then run the program with go runthe command and enter in the terminal:

$ go run hello.go

  1. Specify environment variables

One or more environment variables can be set using -ethe flag .

$ go run -e GOPATH=$HOME/go hello.go

Next, look at the execution process of go run. What exactly does this command do?

Detailed explanation: the execution process of the go run command

go runThe execution process of the command can be divided into the following steps:

  1. The Go toolchain will automatically find all files .goending and compile them into an executable file. If there are multiple files, the dependencies are sorted according to the file names.
  2. After compilation, the Go toolchain loads the resulting binary into memory and uses the runtime environment to run the program.
  3. Go runtime initializes some necessary resources, such as Goroutine scheduler, garbage collector, etc.
  4. The program starts executing main()the function , and the command-line arguments are stored os.Argsin the slice.
  5. If the program needs to read user input from standard input, the Go runtime blocks waiting for user input.
  6. When the program finishes executing or encounters an abnormal situation (such as panic, os.Exit), the Go runtime will release the allocated resources and return control to the operating system.

In summary, go runthe command actually compiles the Go source code into an executable and runs that executable directly. Since no intermediate files are generated, the process is very fast and convenient.

Execution steps of go run command

The first step is to compile: compile into a .a file

The second step is link: Link other .a files to become exe files

The third step is exec: execute the exe file

Example:

package main

import "fmt"

func main() {
    
    
	fmt.Println("Hello, World!")

	var input string
	fmt.Scanln(&input)

	fmt.Println("你的输入是:%s", input)

}

What are .a files?

In Golang, .a files are static library files , also known as archives .

A .a file contains a set of compiled object files (.o files) and an index table for quick lookup of symbols within them. When the linker needs to use symbols from these object files, it can extract the required object files from the .a file and link them with other object files and libraries.

Using a static library file can package the code into a single file for easy distribution and use.

The .a file is generated during the compilation process. Each package will generate a corresponding .a file. When Go compiles, it will first determine whether the source code of the package has changed. If not, it will not recompile the .a file. This can Boost.

When linking, the linker will copy the code in the static library file into the final executable file, so the executable file does not need to depend on external library files.

However, the disadvantage of static library files is that they increase the size of the executable and require the entire program to be recompiled every time the library files are updated.

What is the difference between go run source code and source code compiled and then executed?

1. If the source code is compiled and then executed, the details of the execution process of Go are as follows:

  • .go file (source file) --> go build (compile) --> executable file (.exe or executable file) --> run --> result

2. If we directly execute the go run source code on the source code, the details of the execution process of Go are as follows:

  • .go file (source file) --> go run (compile and run the next step) --> result
  • The difference between the two compilation methods is that the first one is to generate an exe executable file through go build, and the result is run by running the exe binary file, and the second is to run it directly by using go run, and the first one will directly run the time Fast, the second one will be longer than the first one, because compiling and running are put together.

Question: What if you see the execution process of go run? You can use the -n option.

go run -nThe command can print out the compiling and linking process executed by go runthe command without running the program. This command is very useful to check whether the code is compiled and linked correctly and whether the dependent libraries are imported correctly.

It should be noted thatgo run -n the command just compiles, assembles, and links to the console, and does not run the resulting executable. If you need to run the program, use go runthe command or run the generated executable directly.

Here is an example using go run -nthe command :

PS C:\Users\nien\go\src\awesomeProject> go run -n hello.go
mkdir -p $WORK\b001\
cat >$WORK\b001\importcfg.link << 'EOF' # internal
packagefile command-line-arguments=C:\Users\nien\AppData\Local\go-build\69\692a2eab765b2347d83f7dbba3333eeb19fd3151614ee8c7a70f2d5270f9fa95-d
packagefile fmt=C:\Program Files\Go\pkg\windows_amd64\fmt.a
packagefile runtime=C:\Program Files\Go\pkg\windows_amd64\runtime.a
packagefile errors=C:\Program Files\Go\pkg\windows_amd64\errors.a
packagefile internal/fmtsort=C:\Program Files\Go\pkg\windows_amd64\internal\fmtsort.a
packagefile io=C:\Program Files\Go\pkg\windows_amd64\io.a
packagefile math=C:\Program Files\Go\pkg\windows_amd64\math.a
packagefile os=C:\Program Files\Go\pkg\windows_amd64\os.a
packagefile reflect=C:\Program Files\Go\pkg\windows_amd64\reflect.a
packagefile strconv=C:\Program Files\Go\pkg\windows_amd64\strconv.a
packagefile sync=C:\Program Files\Go\pkg\windows_amd64\sync.a
packagefile unicode/utf8=C:\Program Files\Go\pkg\windows_amd64\unicode\utf8.a
packagefile internal/abi=C:\Program Files\Go\pkg\windows_amd64\internal\abi.a
packagefile internal/bytealg=C:\Program Files\Go\pkg\windows_amd64\internal\bytealg.a
packagefile internal/cpu=C:\Program Files\Go\pkg\windows_amd64\internal\cpu.a
packagefile internal/goarch=C:\Program Files\Go\pkg\windows_amd64\internal\goarch.a
packagefile internal/goexperiment=C:\Program Files\Go\pkg\windows_amd64\internal\goexperiment.a
packagefile internal/goos=C:\Program Files\Go\pkg\windows_amd64\internal\goos.a
packagefile runtime/internal/atomic=C:\Program Files\Go\pkg\windows_amd64\runtime\internal\atomic.a
packagefile runtime/internal/math=C:\Program Files\Go\pkg\windows_amd64\runtime\internal\math.a
packagefile runtime/internal/sys=C:\Program Files\Go\pkg\windows_amd64\runtime\internal\sys.a
packagefile internal/reflectlite=C:\Program Files\Go\pkg\windows_amd64\internal\reflectlite.a
packagefile sort=C:\Program Files\Go\pkg\windows_amd64\sort.a
packagefile math/bits=C:\Program Files\Go\pkg\windows_amd64\math\bits.a
packagefile internal/itoa=C:\Program Files\Go\pkg\windows_amd64\internal\itoa.a
packagefile unicode/utf16=C:\Program Files\Go\pkg\windows_amd64\unicode\utf16.a
packagefile unicode=C:\Program Files\Go\pkg\windows_amd64\unicode.a
packagefile internal/race=C:\Program Files\Go\pkg\windows_amd64\internal\race.a
packagefile internal/syscall/windows/sysdll=C:\Program Files\Go\pkg\windows_amd64\internal\syscall\windows\sysdll.a
packagefile path=C:\Program Files\Go\pkg\windows_amd64\path.a
packagefile internal/syscall/windows/registry=C:\Program Files\Go\pkg\windows_amd64\internal\syscall\windows\registry.a
modinfo "0w\xaf\f\x92t\b\x02A\xe1\xc1\a\xe6\xd6\x18\xe6path\tcommand-line-arguments\nbuild\t-compiler=gc\nbuild\tCGO_ENABLED=1\nbuild\tCGO_CFLAGS=\nbuild\tCGO_CPPFLAGS=\nbuild\tCGO_CXXFLAGS=\nbuild\tCGO_LDFLAGS=\nbuild\tGOARCH=amd64\nbuild\tGOOS=windows\nbuild\tGOAMD64=v1\n\xf92C1\x86\x18 r\x00\x82B\x10A\x16\xd8\xf2"
EOF
mkdir -p $WORK\b001\exe\
cd .
"C:\\Program Files\\Go\\pkg\\tool\\windows_amd64\\link.exe" -o "$WORK\\b001\\exe\\hello.exe" -importcfg "$WORK\\b001\\importcfg.link" -s -w -buildmode=pie -buildid=0bpO_A9QPFYvfKrLftok/uW5e8pk9X87KweqPqgKm/3kCXaL5ldAaRrmyF6wVL/0bpO_A9QPFYvfKrLftok -extld=gcc "C:\\Users\\nien\\AppData\\Local\\go-build\\69\\692a2eab765b2347d83f7dbba3333eeb19fd3151614ee8c7a70f2d5270f9fa95-d"
$WORK\b001\exe\hello.exe

Here you can see that two temporary folders b001 and exe have been created, the compile command is executed first, and then link is generated to generate the archive file .a and the final executable file, and the final executable file is placed in the exe folder.

The last step of the command is to execute the executable file.

Detailed explanation of the parameters of go run

Command format: go run [可选参数].
Command function: Compile and run the Go program immediately.
Special note: go run only supports one or more files belonging to the main package as parameters , otherwise it cannot be compiled. For example:

1. 有一个属于包 hello 的文件 hello.go2. 执行编译 go run hello/hello.go 

然后提示错误:go run: cannot run non-main package

Common parameters:

parameter name Format meaning
-o -o file Specify the compiled binary file name
-importcfg -importcfg file read import configuration from file
-s -s omit symbol table and debug info
-w -w omit DWARF symbol table
-buildmode -buildmode mode Set build mode - default is exe
-buildid -buildid id Record the ID as the build ID for the Go toolchain
-extld -extld linker Set external linker - defaults to clang or gcc
-work -work After setting this parameter, the compiled temporary file will not be deleted after the program ends, and can be used to refer to the compiled file
-n -n Add this parameter to view the compilation process, but will not continue to execute the compiled binary file
-x -x Add this parameter to view the compilation process, and continue to execute the compiled binary file

Additional explanations are required for the latter two parameters here.

When compiling with go run, the binary file will be placed in a temporary directory (the location is related to the operating system or GOTMPDIR.

So if you want to view it, you can use the -s or -n command. The simple execution here is as follows:

执行:go run -n -work hello.go   

输出:(不同的环境和版本可能有一些区别)

The above output mainly does these things:

  1. Create temporary directories needed for compiling dependencies. Set a temporary environment variable WORK during compilation for the compiled workspace and execute the compiled binary. Can be set via GOTMPDIR.
  2. Dependencies required for compilation and production compilation. Compile such as standard library, external dependencies, and its own code, and then generate and link the corresponding archive and compile configuration files.
  3. Create an exe directory. Create and enter the snacks directory needed to compile the binary.
  4. Build an executable.
  5. Execute the executable. As listed above: $WORK/b001/exe/hello.exe.

It can be seen that the final go runcommand generates two files, one is an archive file and the other is an executable file.

Where are the archive files?

When the go run command is executed for the second time, if it finds that the imported code package has not changed, then go run will not compile the imported code package again. Direct static link in.

importcfg.linkdocument

importcfg.linkis a new feature introduced in Go 1.17 to specify the behavior of the linker.

importcfg.linkA file is a text file that contains a set of directives that control how a program is linked and its dependencies.

By default, the Go toolchain automatically handles program dependencies and links into executables. But in some cases, such as needing to use a specific linker, linking static libraries, or prohibiting the use of certain libraries, etc., it may be necessary to manually configure the behavior of the linker.

Here is an example of a importcfg.linkfile :

packagefile mypkg.a=/path/to/mypkg.a

import (
    "crypto/tls"
    _ "github.com/mattn/go-sqlite3"
)

unresolved x/x_test.go: import "x" is not a known import path.

In the example above, we use packagefilethe directive to mypkg.aset the path of the static library to /path/to/mypkg.a, then use importthe command to import crypto/tlsthe and github.com/mattn/go-sqlite3packages , and display an unresolved error message to indicate the unknown import path.

**Note that -importcfg** importcfg.linkfiles are only available in versions of the Go toolchain that support the flag, such as Go 1.17 and later. For older versions of the Go toolchain, flags -extldflagssuch as to manually specify linker behavior.

2、go build

The go build command is mainly used for testing compilation. During the compilation of the package, if necessary, the package associated with it will be compiled at the same time.

  1. If it is a normal package, no files will be generated when you execute the go build command.
  2. If it is the main package, when only the go build command is executed, an executable file will be generated in the current directory. If you need to generate the corresponding exe file in the $GOPATH/bin directory, you need to execute go install or use go build -o path/executable file.
  3. The go build command will compile all go files in the current directory by default. If there are multiple files in a folder, and you only want to compile one of them, you can add the file name after go build, for example, go build a.go;
  4. You can also specify a filename for the compiled output. For example, we can specify the executable file name of go build -o, the default is your package name (non-main package), or the file name of the first source file (main package).
  5. go build will ignore go files starting with "_" or "." in the directory.
  6. If your source code needs to be handled differently for different operating systems, then you can name files according to different operating system suffixes.

go build is used to compile the source code files or code packages we specify and their dependent packages. But note that if it is used to compile non-command source code files, that is, library source code files, go build will not produce any results after execution. In this case, the go build command only checks the validity of the library source code files, and only performs check compilation without outputting any result files.

Go build compiles the command source code file, and an executable file will be generated in the execution directory of the command. An example also confirms this process.

After compiling, get the exe file.

Where is the directory? If the directory path is not appended after go build, it will use the current directory as a code package and compile it.

 go build -o bin/hello.exe  hello.go

Detailed explanation: What exactly does go build do?

What exactly does the go build command do?

The execution process of go build is roughly the same as that of go run. The only difference is that in the last step, go run executes the executable file, but the go build command only compiles the library source code file, and then moves the executable file to the current directory. in the folder.

Summarize as follows:

go build command arguments

Command format: go build [可选参数].
Command function: Compile the specified source files, packages and other dependencies, but will not execute binary files after compilation.
Special note: go build and go run are actually similar in the compilation process, the difference is that go build will generate compiled binary files and delete the temporary directory generated during the compilation process. If there is no -o specified file name, it will be consistent with the current directory name.

执行: go build -x main.go

输出:
...
...
mv $WORK/b001/exe/a.out main
// 多了这步
rm -r $WORK/b001/ 

Common parameters:

parameter name Format meaning
-o -o file Specify the compiled binary file name
-a -a Dependencies involved in forced recompilation
-s -s omit symbol table and debug info
-w -w omit DWARF symbol table
-p -p Specify the number of concurrency during compilation, the default is the number of CPUs
-work -work After setting this parameter, the compiled temporary file will not be deleted after the program ends, and can be used to refer to the compiled file
-n -n Add this parameter to view the compilation process, but will not continue to execute the compiled binary file
-x -x Add this parameter to view the compilation process, and continue to execute the compiled binary file

3. go install command

The go install command is used to compile and install code packages or source code files.

The go install command is actually divided into two steps internally:

  • The first step is to generate the result file (executable file or .a package),
  • The second step will move the compiled result to $GOPATH/pkgor $GOPATH/bin​.

In fact, the go install command only does one more thing than the go build command, namely: install the compiled result file to the specified directory.

When is the executable produced? When is the .a application package produced?

  • Executable file: Generally, it is generated by go install go file with main function, and has function entry, all of which can be run directly.
  • .a application package: Generally, it is generated by go install and does not contain the main function of the go file. There is no function entry and can only be called.

go install is used to compile and install the specified code packages and their dependencies. When the dependent packages of the specified code package have not been compiled and installed, this command will process the dependent packages first. As with the go build command, package arguments passed to the go install command should be provided as import paths. Also, most of the flags of the go build command can also be used with

Installing the code package will generate an archive file (that is, a .a file) under the platform-dependent directory of pkg in the current workspace. The installation command source code file will generate an executable file in the bin directory of the current workspace (if there are multiple workspaces under GOPATH, it will be placed in the GOBIN directory).

Similarly, if the go install command does not append any parameters, it will use the current directory as a code package and install it. This is exactly the same as the go build command.

If the go install command is followed by a code package import path as a parameter, the code package and its dependencies will be installed.

If the go install command is followed by command source code files and related library source code files as parameters, only these files will be compiled and installed.

When there is one and only one command source file in the code package, execute the go build command in the directory where the folder is located, and an executable file with the same name as the directory will be generated in this directory.

Before executing go install, in general, you need to set the GOBIN environment variable in advance:

Generate the corresponding executable file under GOBIN.

what is GOBINit

GOBINis an environment variable that specifies where go installthe command installs executables.

In GOBINaddition to , you can use go env to see other environment variables

When we use go installthe command to compile a Go program, the generated executable file will be installed $GOBINin the directory (if not set GOBIN, the default is $GOPATH/binthe directory ).

In Linux or macOS, it can be set by the following command GOBIN:

$ export GOBIN=/path/to/go/bin

On Windows, it can be set with the following command GOBIN:

setx GOBIN "C:\path\to\go\bin"
eg: 
setx GOBIN "C:\Users\nien\go\bin"

Also, if you want to run the installed program directly, you need to pay attention: GOBINit needs to be in the PATH environment variable of the system to work. Therefore, you can add $GOBINor $GOPATH/binto the PATH environment variable, so that you can run the installed program directly in any directory.

Usually, we will $GOPATH/binadd to PATH, so that the installed programs can be used directly in the terminal.

For example:

$ export PATH=$PATH:$GOPATH/bin

or on Windows:

> setx PATH "%PATH%;%GOPATH%\bin"

In this way, the installed program can be run directly in any directory.

Detailed explanation: What exactly does go install do?

What exactly does the go install command do?

The first few steps of go install are still exactly the same as go run and go build, except for the last step.

In the last step, go install will install the command source files to the bin directory of the current workspace (if there are multiple workspaces under GOPATH, they will be placed in the GOBIN directory). If it is a library source code file, it will be installed in the platform-related directory of pkg in the current workspace.

Summarize as follows:

go install command arguments

Command format: go install [可选参数] .
Command function: compile and install source files and software packages, that is, install compiled files (executable binary files, archive files, etc.) into the specified directory.
Special instructions: Install compiled files (executable binaries, archives, etc.) into the specified directory. If the environment variable GOBIN is set, move the binary executable to this directory. Put it $GOPATH/pkg/$GOOS_$GOARCHdown .

执行: go install -x main.go

输出:
...
mkdir -p /Users/ucwords/go/bin/
...
mv $WORK/b001/exe/a.out /Users/ucwords/go/bin/目标目录(go modules的目录名)
rm -r $WORK/b001/ 

Common parameters:

parameter name Format meaning
-o -o file Specify the compiled binary file name
-a -a Dependencies involved in forced recompilation
-s -s omit symbol table and debug info
-w -w omit DWARF symbol table
-p -p Specify the number of concurrency during compilation, the default is the number of CPUs
-work -work After setting this parameter, the compiled temporary file will not be deleted after the program ends, and can be used to refer to the compiled file
-n -n Add this parameter to view the compilation process, but will not continue to execute the compiled binary file
-x -x Add this parameter to view the compilation process, and continue to execute the compiled binary file

4、go get

The go get command is used to download and install code packages from remote code warehouses (such as Github).

Note that the go get command will download the current code package to the src directory of the first workspace in $GOPATH and install it.

When using go get to download a third-party package, it will still be downloaded to the first workspace of $GOPATH instead of the vendor directory. There is no real package dependency management in the current work chain, but fortunately, there are many third-party tools to choose from.

如果在 go get 下载过程中加入-d 标记,那么下载操作只会执行下载动作,而不执行安装动作。比如有些非常特殊的代码包在安装过程中需要有特殊的处理,所以我们需要先下载下来,所以就会用到-d 标记。

还有一个很有用的标记是-u标记,加上它可以利用网络来更新已有的代码包及其依赖包。如果已经下载过一个代码包,但是这个代码包又有更新了,那么这时候可以直接用-u标记来更新本地的对应的代码包。如果不加这个-u标记,执行 go get 一个已有的代码包,会发现命令什么都不执行。只有加了-u标记,命令会去执行 git pull 命令拉取最新的代码包的最新版本,下载并安装。

命令 go get 还有一个很值得称道的功能——智能下载。在使用它检出或更新代码包之后,它会寻找与本地已安装 Go 语言的版本号相对应的标签(tag)或分支(branch)。比如,本机安装 Go 语言的版本是1.x,那么 go get 命令会在该代码包的远程仓库中寻找名为 “go1” 的标签或者分支。如果找到指定的标签或者分支,则将本地代码包的版本切换到此标签或者分支。如果没有找到指定的标签或者分支,则将本地代码包的版本切换到主干的最新版本。

go get 常用的一些标记如下:

标记名称 标记描述
-d 让命令程序只执行下载动作,而不执行安装动作。
-f 仅在使用-u标记时才有效。该标记会让命令程序忽略掉对已下载代码包的导入路径的检查。如果下载并安装的代码包所属的项目是你从别人那里 Fork 过来的,那么这样做就尤为重要了。
-fix 让命令程序在下载代码包后先执行修正动作,而后再进行编译和安装。
-insecure 允许命令程序使用非安全的 scheme(如 HTTP )去下载指定的代码包。如果你用的代码仓库(如公司内部的 Gitlab )没有HTTPS 支持,可以添加此标记。请在确定安全的情况下使用它。
-t 让命令程序同时下载并安装指定的代码包中的测试源码文件中依赖的代码包。
-u 让命令利用网络来更新已有代码包及其依赖包。默认情况下,该命令只会从网络上下载本地不存在的代码包,而不会更新已有的代码包。

总结一下如下图:

5、其他命令

go clean

go clean 命令是用来移除当前源码包里面编译生成的文件,这些文件包括

  • _obj/ 旧的object目录,由Makefiles遗留
  • _test/ 旧的test目录,由Makefiles遗留
  • _testmain.go 旧的gotest文件,由Makefiles遗留
  • test.out 旧的test记录,由Makefiles遗留
  • build.out 旧的test记录,由Makefiles遗留
  • *.[568ao] object文件,由Makefiles遗留
  • DIR(.exe) 由 go build 产生
  • DIR.test(.exe) 由 go test -c 产生
  • MAINFILE(.exe) 由 go build MAINFILE.go产生

go fmt

go fmt 命令主要是用来帮你格式化所写好的代码文件。

比如我们写了一个格式很糟糕的 test.go 文件,我们只需要使用 fmt go test.go 命令,就可以让go帮我们格式化我们的代码文件。

但是我们一般很少使用这个命令,因为我们的开发工具一般都带有保存时自动格式化功能,这个功能底层其实就是调用了 go fmt 命令而已。

使用go fmt命令,更多时候是用gofmt,而且需要参数-w,否则格式化结果不会写入文件。gofmt -w src,可以格式化整个项目。

go test 命令

go test 命令是 Go 语言自带的一个测试工具,用于执行程序中的单元测试和性能测试。

执行 go test 命令时,它会自动查找当前目录及其子目录下的所有以 _test.go 结尾的文件,并执行其中的测试函数。

go test 命令支持多种参数和选项,可以通过 go help test 查看完整的帮助文档。其中一些常用的参数和选项包括:

-v:输出详细的测试日志信息,包括测试用例的名称、运行时间、每个测试函数的输出结果等。
-run:指定需要运行的测试函数的名称或正则表达式。
-cover:生成代码覆盖率报告,报告中会显示哪些代码被测试覆盖到了,哪些没有被覆盖到。
-bench:执行性能测试,并输出测试结果。可以指定 -benchmem 选项来输出内存分配的情况。
例如,执行 go test -v ./… 命令将会递归地运行当前目录及其子目录下所有的测试函数,并输出详细的测试日志信息。

go doc 命令

作用:打印出程序实体说明文档。后可不跟参数或一个参数或两个参数
格式:go doc 标记 参数
标记和参数可以不填,

  • go doc
    在 main 包下,执行 go doc 默认是不打印的,除非加上 -cmd 标记,后面会讲
    在非 main 包下,执行 go doc 打印当前代码包文档及其程序实体列表(程序实体:变量、常量、函数、结构体以及接口)
  • go doc 标记
    标记有如下:
标记 含义
-c 区分后跟参数的大小写,比如:go doc -c packageOne(默认不写是不区分大小写 )
-cmd 加入此标记会使go doc命令同时打印出main包中的可导出的程序实体(其名称的首字母大写)的文档。默认下,这些文档是不会被打印的。
-u 加入此标记后会使go doc命令同时打印出不可导出的程序实体(其名称的首字母小写)的文档。默认下,这部分文档是不会被打印出来的。
  • go doc 参数
    go doc 参数, 比如:go doc http.Request 会输出 http 包下 Request 文档说明,也可以跟两个参数,见下
    go doc 参数1 参数2,比如:go doc net/http Request ,需要说明的是第一个参数要写完整的导入路径,我个人理解就是在参数1 的范围下,打印出参数2的文档说明详情,其实此时 doc 和 参数之间还可以加入标记,相当于打印文档时又加入了条件判断。

go fix

用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1

go version

查看go当前的版本

go env

查看当前go的环境变量

go list

列出当前全部安装的package

golang 代码规范

命名在所有的编程语言中都是有规则可寻的,也是需要遵守的,只有我们有了好的命名习惯才可以写出好的代码,例如我们在生活中对建筑的命名也是希望可以表达这个建筑的含义和作用。

在Go语言中也是一样的,Go语言的函数名,变量名,常量名,类型名和包的命名也是都遵循这一规则的:一个一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。

大写字母和小写字母是不同的:Car和car是两个不同的名字。

Go语言中也有类似java的关键字,且关键字不能用于自定义名字,只能在特定语法结构中使用.

break      default        func     interface    select
case       defer          go       map          struct
chan       else           goto     package      switch
const      fallthrough    if       range        type
continue   for            import   return       var

除此之外Go语言中还有30多个预定义的名字,比如int和ture等

内建常量: true false iota nil
内建类型: int   int8   int16   int32  int64   uint   uint8  uint16   uint32   uint64   uintptr  
         float32   float64   complex128   complex64   bool   byte    rune   string  error
内建函数: make  len  cap   new   append   copy   close   delete   complex   real    imag    panic  recover

通常我们在Go语言编程中推荐的命名方式是驼峰命名例如:ReadAll,不推荐下划线命名。

好的规范的价值

对于团队而言,好的规范虽一定程度降低开发自由度,但带来的好处是不可忽视的

  • 可以减少 bug 的产生
  • 降低 review 和接手成本,通过统一规范,每个人代码风格统一,理想情况看谁的代码就像自己写的一样
  • 利于写一些片段脚本
  • 提高代码可读性

下面总结了平时项目中必须遵守的规范,后续会不断更新,也可以来尼恩的 高并发社群《技术自由圈(原名:疯狂创客圈)》 中交流。

Import 规范

  • 原则上遵循 goimports 规范,goimports 会自动把依赖按首字母排序,并对包进行分组管理,通过空行隔开,默认分为本地包(标准库、内部包)、第三方包。
  • 标准包永远位于最上面的第一组
  • 使用完整路径,不要使用相对路径
  • 包名和 git 路径名不一致时,或者多个相同包名冲突时,使用别名代替

错误处理

  • error 作为函数的值返回,必须对 error 进行处理,或将返回值赋值给明确忽略。
  • error 作为函数的值返回且有多个返回值的时候,error 必须是最后一个参数。
// 不建议
func do() (error, int) {
    
    
}
// 建议
func do() (int, error) {
    
    
}
  • 优先处理错误,能 return 尽早 return。理想情况代码逻辑是平铺的顺着读就能看懂,过多的嵌套会降低可读性
// 不建议
if err != nil {
    
    
       // error handling     
} else {
    
    
       // normal code     
}

// 建议
if err != nil {
    
    
   // error handling
  return // or continue, etc.
}
// normal code
  • 错误返回优先独立判断,不与其他变量组合判断
// 不建议 
x, y, err := f()     
if err != nil || y == nil {
    
    
  return err   // 当y与err都为空时,函数的调用者会出现错误的调用逻辑     
}

// 建议
x, y, err := f()
if err != nil {
    
    
  return err
}
if y == nil {
    
    
  return fmt.Errorf("some error")
}

panic

  • 在业务逻辑处理中禁止使用 panic。
  • 在 main 包中只有当完全不可运行的情况可使用 panic,例如:文件无法打开,数据库无法连接导致程序无法正常运行。
  • 对于其它的包,可导出的接口不能有 panic,只能在包内使用。
  • 建议在 main 包中使用 log.Fatal 来记录错误,这样就可以由 log 来结束程序,或者将 panic 抛出的异常记录到日志文件中,方便排查问题。
  • panic 捕获只能到 goroutine 最顶层,每个自行启动的 goroutine,必须在入口处捕获 panic,并打印详细堆栈信息或进行其它处理。

recover

  • recover 用于捕获 runtime 的异常,禁止滥用 recover。
  • 必须在 defer 中使用,一般用来捕获程序运行期间发生异常抛出的 panic 或程序主动抛出的 panic。

单元测试

  • 单元测试文件名命名规范为 example_test.go。
  • 测试用例的函数名称必须以 Test 开头,例如 TestExample。
  • 如果存在 func Foo,单测函数可以带下划线,为 func Test_Foo。如果存在 func (b *Bar) Foo,单测函数可以为 func TestBar_Foo。下划线不能出现在前面描述情况以外的位置。
  • 每个重要的可导出函数都要首先编写测试用例,测试用例和正规代码一起提交方便进行回归测试。

类型断言失败处理

type assertion 的单个返回值形式针对不正确的类型将产生 panic。

因此,请始终使用 “comma ok” 的惯用法。

// 不建议
t := i.(string)

// 建议
t, ok := i.(string)
if !ok {
    
    
  // 优雅地处理错误
}

注释

  • 在编码阶段同步写好变量、函数、包注释,注释可以通过 godoc 导出生成文档。
  • 程序中每一个被导出的(大写的)名字,都应该有一个文档注释。
  • 所有注释掉的代码在提交 code review 前都应该被删除,除非添加注释讲解为什么不删除, 并且标明后续处理建议(比如删除计划)。

命名规范

  • 文件名应该采用小写,并且使用下划线分割各个单词,文件名尽量采用有意义简短的文件
  • 结构体,接口,变量,常量,函数均采用驼峰命名

命名规范设计变量、常量、全局函数、结构、方法等等的命名。

Go语言从语法层面进行了以下限定:任何需要对外暴露的名字必须以大写字母开头,不需要对外暴露的则以小写字母开头。

当一个命名以一个大写字母开头,如GetUserName,那么使用这种形式的标识符的对象就可以被外部包的代码使用(客户端程序需要先导入这个包),这被称为导出(如面向对象语言中的public);命名如果以小写字母开头,则对包外是不可兼得,但是他们在整个包的内部是可见的并且可用的(如面向对象语言中的private)

包命名

保持package的名字和目录保持一致,尽量采用有意义的包名,简短、有意义且尽量和标准库不要冲突。

包命应该为小写单词,不要使用下划线或者混合大小写。

package psych
package service

文件命名

尽量采用有意义且简短的文件名,应为小写单词,使用下划线分隔各个单词

customer_dao.go

结构体命名

采用驼峰命名法,首字母根据访问控制大小写

struct 声明和初始化格式采用多行,例:

type CustomerOrder struct{
    
    
    Name string
    Address string
}
order := CustomerOder{
    
    "psych","四川成都"}

接口命名

命名规则基本上与上面结构体类似

单个函数的结构名以“er”作为后缀,例如ReaderWriter

type Reader interface{
    
    
    Read(p []byte)(n int,err error)
}

首字母访问控制规则

在 Golang 中,如果一个标识符(如变量、函数名等)的首字母大写,表示它是可导出的(exported),即可以被外部包访问和使用;如果首字母小写,则表示它是不可导出的(unexported),只能在当前包内部使用。

这是因为 Golang 的访问控制是基于标识符的命名规则来实现的,首字母大小写的不同决定了该标识符的可见性。具体来说,对于一个标识符,如果它的首字母大写,那么它就是公开(public)的,可以被其他包导入后直接使用;如果首字母小写,那么它就是私有(private)的,只能在当前包内部使用,对于其他包来说是不可见的。

举个例子,假设我们有一个包叫做"example",其中定义了一个变量"Name":

package example

var Name string = "hello"

由于变量"Name"的首字母大写,所以它是可导出的,可以被其他包导入后直接使用:

package main

import (
    "fmt"
    "example"
)

func main() {
    
    
    fmt.Println(example.Name) // 输出:hello
}

但如果把"Name"的首字母改为小写,那么它就是不可导出的,外部包无法访问和使用它:

package example

var name string = "hello"

package main

import (
    "fmt"
    "example"
)

func main() {
    
    
    fmt.Println(example.name) // 编译错误:name undefined (cannot refer to unexported name example.name)
}

因此,在 Golang 中,通过标识符的命名规则来实现访问控制,可以有效地保障代码的封装性和安全性。

方法命名

Golang 中的方法命名遵循一般的命名规则,遵守首字母大小访问控制规则,同时,也建议采用驼峰式命名法(camel case)。

在 Golang 中,方法通常是与某个类型(结构体、接口等)关联的函数。方法名应该简洁明了,描述清楚该方法的作用和功能,通常使用动词加上一定的描述或说明来命名。

以下是一些常见的方法命名规范:

  • GetXxx:表示获取某个属性或值,例如 GetName 表示获取名称。
  • SetXxx:表示设置某个属性或值,例如 SetAge 表示设置年龄。
  • AddXxx:表示添加某个元素或对象,例如 AddItem 表示添加一个元素。
  • RemoveXxx:表示移除某个元素或对象,例如 RemoveItem 表示移除一个元素。
  • DoXxx:表示执行某个操作,例如 DoSomething 表示执行某个操作。
  • XxxWithYyy:表示使用 Yyy 作为参数执行 Xxx 操作,例如 WriteWithTimeout 表示使用超时参数执行写操作。

需要注意的是,方法名应该尽量避免使用缩写或缩略语,除非是广泛使用的常见缩写,否则容易引起歧义和误解。同时,方法名也应该尽量避免冗长,以保持代码的简洁性和可读性。

举个例子,假设我们有一个结构体叫做"Person",它有一个方法"SayHello":

type Person struct {
    
    
    Name string
    Age  int
}

func (p *Person) SayHello() {
    
    
    fmt.Printf("Hello, my name is %s and I am %d years old.\n", p.Name, p.Age)
}

在上面的例子中,我们使用了驼峰式命名法来命名方法"SayHello",同时结合动词和名词来描述该方法的作用。这样做可以使代码更易读、易懂。

变量命名

和结构体类似,一般遵循驼峰命名法,首字母根据访问控制大小写,但遇到特有名词时,需要遵循以下规则:

如果变量为私有,且特有名词为首个单词,则使用小写,如 appService

若变量为bool类型,则名称应以has、is、can或allow开头

var isExist bool
var hasConflict bool
var canManage bool
var allowGitHook bool

常量命名

常量需使用全部大写字母组成,并使用下划线分词

const APP_URL = "https://www.baidu.com"

如果是枚举类型的常量,需要先创建对应类型

type Scheme string

const{
    
    
    HTTP Scheme = "http"
    HTTPS Scheme = "https"
}

单元测试命名

在 Golang 中,单元测试采用的是内置的 testing 包,我们需要遵循一定的命名规范来编写测试函数的名称。Golang 的测试函数命名规则是:

  • 测试函数必须以 Test 开头,例如 TestAdd。
  • 测试函数参数列表中必须有一个类型为 *testing.T 的参数,例如 func TestAdd(t *testing.T)。
  • 测试函数的参数列表中不应该包含任何其他参数。
  • 测试函数的名称应该具有描述性,可以使用驼峰式命名法,例如 TestAddTwoNumbers。

另外,对于某个包下的所有测试函数,我们可以将它们组织成一个表格测试(Table-Driven Tests)的形式,这样可以使测试代码更加简洁、清晰和易于维护。表格测试的命名规则如下:

  • 表格测试函数必须以 Test 开头,例如 TestAddTable。
  • 表格测试函数参数列表中必须有一个类型为 *testing.T 的参数,例如 func TestAddTable(t *testing.T)。
  • 表格测试函数的参数列表中不应该包含任何其他参数。
  • 表格测试函数应该包含一个结构体切片或映射作为输入参数,同时也需要定义一个期望输出结果的切片或映射,例如:
func TestAddTable(t *testing.T) {
    
    
    tests := []struct {
    
    
        a, b int
        want int
    }{
    
    
        {
    
    1, 2, 3},
        {
    
    0, 0, 0},
        {
    
    -1, -1, -2},
    }
    for _, tt := range tests {
    
    
        got := Add(tt.a, tt.b)
        if got != tt.want {
    
    
            t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.want)
        }
    }
}

在这个例子中,我们使用了表格测试的形式来测试 Add 函数的功能和正确性。其中,tests 切片包含了多组输入参数和期望输出结果,每次循环都会进行一次测试,并将实际输出结果与期望输出结果进行比较,如果不一致则使用 t.Errorf 输出错误信息和相关参数。

需要注意的是,单元测试应该覆盖代码中的所有核心功能和可能出现的异常情况,以保证代码的质量和可靠性。同时,测试代码也需要遵循相应的编程规范和最佳实践,提高测试代码的可读性和可维护性。

控制结构

  • if 语句
// 不建议,变量优先在左
if nil != err {
    
    
}
// 建议这种
if err != nil {
    
    
}   

// 不建议,bool类型变量直接进行
if has == true {
    
    
}
// 建议
if has {
    
    
}
  • switch 语句,必须有 default 哪怕什么都不做
  • 业务代码禁止使用 goto,其他框架或底层源码推荐尽量不用。

函数

函数返回相同类型的两个或三个参数,或者如果从上下文中不清楚结果的含义,使用命名返回,其它情况不建议使用命名返回。

func (n *Node) Parent1() *Node

func (n *Node) Parent2() (*Node, error)

func (f *Foo) Location() (lat, long float64, err error)

Defer

  • 当存在资源管理时,应紧跟 defer 函数进行资源的释放。
  • 判断是否有错误发生之后,再 defer 释放资源。
resp, err := http.Get(url)
if err != nil {
    
    
   return err     
}     
// defer 放到错误处理之后,不然可能导致panic     
defer resp.Body.Close() 
  • 禁止在循环中的延迟函数中使用 defer,因为 defer 的执行需要外层函数的结束才会释放,未来会有很多坑
// 不要这样使用
func filterSomething(values []string) {
    
    
  for _, v := range values {
    
    
    fields, err := db.Query(xxx)         
    if err != nil {
    
    
    }         
    defer fields.Close()
    // xxx         
  }     
}

// 但是可以使用这种方式
func filterSomething(values []string) {
    
    
  for _, v := range values {
    
    
    func() {
    
    
      fields, err := db.Query(xxx)
      if err != nil {
    
    
        // xxx
      }
      defer fields.Close()
      //x xxx
    }()
  }
}

魔法数字

如果魔法数字出现超过 2 次,则禁止使用。

func getArea(r float64) float64 {
    
    
    return 3.14 * r * r
}

func getLength(r float64) float64 {
    
    
    return 3.14 * 2 * r
}

// 建议定义一个常量代替魔法数字
// PI xxx
const PI = 3.14

代码规范性常用工具

上面提到了很过规范, go 语言本身在代码规范性这方面也做了很多努力,很多限制都是强制语法要求,例如左大括号不换行,引用的包或者定义的变量不使用会报错,此外 go 还是提供了很多好用的工具帮助我们进行代码的规范,

gofmt
大部分的格式问题可以通过gofmt解决, gofmt 自动格式化代码,保证所有的 go 代码与官方推荐的格式保持一致,于是所有格式有关问题,都以 gofmt 的结果为准。

goimport
我们强烈建议使用 goimport ,该工具在 gofmt 的基础上增加了自动删除和引入包.

go get golang.org/x/tools/cmd/goimports

go vet
vet工具可以帮我们静态分析我们的源码存在的各种问题,例如多余的代码,提前return的逻辑,struct的tag是否符合标准等。

go get golang.org/x/tools/cmd/vet

使用如下:

go vet .

包和文件

每个 Go 程序都是由包构成的。程序从 main 包开始运行。

本程序通过导入路径 "fmt""math/rand" 来使用这两个包。

import (
	"fmt"
	"math/rand"
)

func main() {
    
    
	fmt.Println("Hello, World!")

	var input string
	fmt.Scanln(&input)

	fmt.Println("你的输入是:%s", input)
	fmt.Println("My favorite number is", rand.Intn(10))

}

按照约定,包名与导入路径的最后一个元素一致

例如,"math/rand" 包中的源码均以 package rand 语句开始。

注意: 此程序的运行环境是固定的,因此 rand.Intn 总是会返回相同的数字。

要得到不同的数字,需为生成器提供不同的种子数,参见 rand.Seed

包的价值: 进行模块的隔离

Go语言中的包和其他语言的库或模块的概念类似,目的都是为了支持模块化、封装、单独编译和代码重 用。

一个包的源代码保存在一个或多个以.go为文件后缀名的源文件中. 在Go语言中包还可以让我们通过控制哪些名字是外部可见的来隐藏内部实现信息。

在Go语言中,一个简单的规则 是:如果一个名字是大写字母开头的,那么该名字是导出的。类似于Java 中的public

如果包中含有多个.go源文件,它们将按照发给编译器的顺序进行初始化,Go语言的构建工具首先会 将.go文件根据文件名排序,然后依次调用编译器编译。

对于在包级别声明的变量,如果有初始化表达式则用表达式初始化,还有一些没有初始化表达式的,例 如某些表格数据初始化并不是一个简单的赋值过程。

包的init 初始化函数

在这种情况下,我们可以用一个特殊的init初始化 函数来简化初始化工作。每个文件都可以包含多个init初始化函数

func init() {
    
     }

这样的init初始化函数除了不能被调用或引用外,其他行为和普通函数类似。

在每个文件中的init初始 化函数,在程序开始执行时按照它们声明的顺序被自动调用。

每个包在解决依赖的前提下,以导入声明的顺序初始化,每个包只会被初始化一次。

因此,如果一个p包 导入了m包,那么在p包初始化的时候可以认为m包必然已经初始化过了。

初始化工作是自下而上进行的, main包最后被初始化。

以这种方式,可以确保在main函数执行之前,所有依然的包都已经完成初始化工 作了。

包的导入

import导入包的用法:

import       "github.com/tidwall/gjson"   //通过包名gjson调用导出接口
import  json "github.com/tidwall/gjson"   //通过别名json调用gjson
import     . "github.com/tidwall/gjson"   //.符号表示,对包gjson的导出接口的调用直接省略包名
import    _  "github.com/tidwall/gjson"   //_ 仅仅会初始化gjson,如初始化全局变量,调用init函数

当然你也可以编写多个导入语句,例如:

import "fmt"
import "math"

此代码用圆括号组合了导入,这是“分组”形式的导入语句。不过使用分组导入语句是更好的形式。

imports.go

import (
	"fmt"
	"math/rand"
)

func main() {
    
    
	fmt.Println("Hello, World!")

	var input string
	fmt.Scanln(&input)

	fmt.Println("你的输入是:%s", input)
	fmt.Println("My favorite number is", rand.Intn(10))

}

作用域

一个声明语句将程序中的实体和一个名字关联,比如一个函数或一个变量。

声明语句的作用域是指源代 码中可以有效使用这个名字的范围。

不要将作用域和生命周期混为一谈。

声明语句的作用域对应的是一个源代码的文本区域;它是一个编译 时的属性。

一个变量的生命周期是指程序运行时变量存在的有效时间段,在此时间区域内它可以被程序 的其他部分引用;是一个运行时的概念。

语法块是由花括弧所包含的一系列语句,就像函数体或循环体花括弧对应的语法块那样。语法块内部声 明的名字是无法被外部语法块访问的。

语法决定了内部声明的名字的作用域范围。我们可以这样理解, 语法块可以包含其他类似组批量声明等没有用花括弧包含的代码,我们称之为语法块

有一个语法块为 整个源代码,称为全局语法块;然后是每个包的包语法决;每个for、if和switch语句的语法决;每个 switch或select的分支也有独立的语法决;当然也包括显式书写的语法块(花括弧包含的语句)。

声明语句对应的词法域决定了作用域范围的大小。对于内置的类型、函数和常量,比如int、len和true 等是在全局作用域的,因此可以在整个程序中直接使用。

任何在在函数外部(也就是包级语法域)声明 的名字可以在同一个包的任何源文件中访问的。

对于导入的包,例如tempconv导入的fmt包,则是对应源 文件级的作用域,因此只能在当前的文件中访问导入的fmt包,当前包的其它源文件无法访问在当前源文 件导入的包

在包级别,声明的顺序并不会影响作用域范围,因此一个先声明的可以引用它自身或者是引用后面的一 个声明,这可以让我们定义一些相互嵌套或递归的类型或函数。

但是如果一个变量或常量递归引用了自 身,则会产生编译错误。

if f, err := os.Open(fname); err != nil {
    
     // compile error: unused: f
    return err
}
f.ReadByte() // compile error: undefined f
f.Close() // compile error: undefined f

变量f的作用域只有在if语句内,因此后面的语句将无法引入它,这将导致编译错误。

你可能会收到一个 局部变量f没有声明的错误提示,具体错误信息依赖编译器的实现。

文件命名

  1. golang的命名需要使用驼峰命名法,且不能出现下划线
  2. golang中根据首字母的大小写来确定可以访问的权限。
    无论是方法名、常量、变量名还是结构体的名称,如果首字母大写,则可以被其他的包访问;
    如果首字母小写,则只能在本包中使用,可以简单的理解成,首字母大写是公有的,首字母小写是私有的
  3. 结构体中属性名的大写
    如果属性名小写则在数据解析(如json解析,或将结构体作为请求或访问参数)时无法解析

在golang源代码中,经常看到各种文件名,比如: bolt_windows.go。

下面对文件名命令规则的说明:

  1. 平台区分
    文件名_平台。
    例: file_windows.go, file_unix.go
    可选为windows, unix, posix, plan9, darwin, bsd, linux, freebsd, nacl, netbsd, openbsd, solaris, dragonfly, bsd, notbsd, android,stubs
  2. 测试单远
    文件名test.go或者 文件名平台_test.go。
    例: path_test.go, path_windows_test.go
  3. 版本区分(猜测)
    文件名_版本号等。
    例:trap_windows_1.4.go
  4. CPU类型区分, 汇编用的多
    文件名_(平台:可选)_CPU类型.
    例:vdso_linux_amd64.go
    可选为amd64, none, 386, arm, arm64, mips64, s390,mips64x,ppc64x, nonppc64x, s390x, x86,amd64p32

函数

函数可以没有参数或接受多个参数。

在本例中,add 接受两个 int 类型的参数。

注意, go和java不同,函数的返回类型,在函数名 之后

Functions.go

package basic

import "fmt"

func Add(a, b int) int {
    
    
	var ret = a + b
	fmt.Printf("The sum of %d and %d is: %d\n", a, b, ret)
	return ret

}
func Callback(y int, f func(int, int) int) {
    
    
	f(y, 5) // this becomes Add(1, 5)
}
func DoCallback(y int) {
    
    
	Callback(y, Add) // this becomes Add(1, 5)
}

当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。

在本例中,

func Add(a int, b int) int {
    
    
....
}

其中的两个参数

a int, b int

被缩写为

a, b int

函数定义:

函数构成代码执行的逻辑结构。

在Go语言中,函数的基本组成为:关键字 func 、函数名、参数列表、返回值、函数体和返回语句。

除了main()、init()函数外,其它所有类型的函数都可以有参数与返回值。

函数参数、返回值以及它们的类型被统称为函数签名。

func function_name( [parameter list] ) [return_types] {
    
    
   body(函数体)
}

函数定义:

  • func:函数由 func 开始声明
  • function_name:函数名称,函数名和参数列表一起构成了函数签名。
  • parameter list:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
  • return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。
  • body(函数体):函数定义的代码集合。

形式参数列表描述了函数的参数名以及参数类型。

这些参数作为局部变量,其值由参数调用者提供。返回值列表描述了函数返回值的变量名以及类型。

如果函数返回一个无名变量或者没有返回值,返回值列表的括号是可以省略的。

如果一个函数声明不包括返回值列表,那么函数体执行完毕后,不会返回任何值。

// 这个函数计算两个int型输入数据的和,并返回int型的和
func Add(a int, b int) int {
    
    
    // Go需要使用return语句显式地返回值
    return a + b
}

fmt.Println(Add(3,4)) //函数的调用

在Add函数中a和b是形参名3和4是调用时的传入的实数,函数返回了一个int类型的值。

返回值也可以像形式参数一样被命名。

在这种情况下,每个返回值被声明成一个局部变量,并根据该返回值的类型,将其初始化为0。

如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾,除非函数明显无法运行到结尾处。

例如函数在结尾时调用了panic异常或函数中存在无限循环。

函数如何调用

那么函数在使用的时候如何被调用呢?

package.Function(arg1, arg2,, argn)

Function 是 package包里面的一个函数,括号里的是被调用函数的实参(argument):这些值被传递给被调用函数的形参(parameter)。

函数被调用的时候,这些实参将被复制然后传递给被调用函数。

函数一般是在其他函数里面被调用的,这个其他函数被称为调用函数(calling function)。

函数能多次调用其他函数,这些被调用函数按顺序行,理论上,函数调用其他函数的次数是无穷的(直到函数调用栈被耗尽)。

函数的变长参数

在函数中有的时候可能也会遇到你要传递的参数的类型是多个(传递变长参数)如这样的函数:

func patent(a,b,c ...int)

参数是采用 …type 的形式传递,这样的函数称为变参函数.

只有声明没有body的函数

你可能会偶尔遇到没有函数体的函数声明,这表示该函数不是以Go实现的。

这样的声明定义了函数标识符。

package math
func Sin(x float64) float //implemented in assembly language

返回多值的函数

函数可以返回任意数量的返回值。

swap 函数返回了两个字符串。


//函数定义
func Swap(x, y string) (string, string) {
    
    
	return y, x
}

//函数调用
func main() {
    
    
	basic.BasicTypeDemo()
	basic.BasicTypeDemo()
	basic.DoCallback(1)
	basic.Callback(1, basic.Add)

	a, b := basic.Swap("hello", "world")
	fmt.Println(a, b)
}

命名返回值

Go 的返回值可被命名,它们会被视作定义在函数顶部的变量

返回值的名称应当具有一定的意义,它可以作为文档使用。

没有参数的 return 语句,返回已命名的返回值。也就是 直接 返回。

func Split(sum int) (x, y int) {
    
    
	x = sum * 4 / 9
	y = sum - x
	return
}

函数的使用

x, y := basic.Split(40)
fmt.Println(x, y)

直接返回语句应当仅用在上面这样的短函数中。在长的函数中它们会影响代码的可读性。

匿名返回值

函数签名中命名返回值变量,只指定返回值类型。由return 指定返回值。

// 匿名返回值
func Split02(sum int) (int, int) {
    
    
	var x = sum * 4 / 9
	var y = sum - x
	return x, y
}

任何一个非命名返回值(使用非命名返回值是很糟的编程习惯)在 return 语句里面都要明确指出包含返回值的变量或是一个可计算的值(就像上面警告所指出的那样)

建议:尽量使用命名返回值:会使代码更清晰、更简短,同时更加容易读懂。

将函数作为参数传递

在 Go 语言中,函数是一等公民,也就是说,函数可以像值一样传递和使用。

因此,可以将函数作为参数传递给其他函数,实现更加灵活的编程方式。

func Add(a, b int) int {
    
    
	var ret = a + b
	fmt.Printf("The sum of %d and %d is: %d\n", a, b, ret)
	return ret

}
func Callback(y int, f func(int, int) int) {
    
    
	f(y, 5) // this becomes Add(1, 5)
}
func DoCallback(y int) {
    
    
	Callback(y, Add) // this becomes Add(1, 5)
}

调用方法

basic.DoCallback(1)
basic.Callback(1, basic.Add)

输出结果为:

The sum of 1 and 5 is: 6
The sum of 1 and 5 is: 6

内置函数

Go语言拥有一些不需要进行导入操作就可以使用的内置函数。

它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因而,它们需要直接获得编译器的支持。

名称 说明
close 用于关闭管道通信channel
len、cap len 用于返回某个类型的长度或数量(字符串、数组、切片、map 和管道);cap 是容量的意思,用于返回某个类型的最大容量(只能用于切片和 map)
new、make new 和 make 均是用于分配内存:new 用于值类型和用户定义的类型,如自定义结构,内建函数new分配了零值填充的元素类型的内存空间,并且返回其地址,一个指针类型的值。make 用于内置引用类型(切片、map 和管道)创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。。它们的用法就像是函数,但是将类型作为参数:new(type)、make(type)。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针)。它也可以被用于基本类型:v := new(int)。make(T) 返回类型 T 的初始化之后的值,因此它比 new 进行更多的工作,new() 是一个函数,不要忘记它的括号
copy、append copy函数用于复制,copy返回拷贝的长度,会自动取最短的长度进行拷贝(min(len(src), len(dst))),append函数用于向slice追加元素
panic、recover 两者均用于错误处理机制,使用panic抛出异常,抛出异常后将立即停止当前函数的执行并运行所有被defer的函数,然后将panic抛向上一层,直至程序carsh。recover的作用是捕获并返回panic提交的错误对象、调用panic抛出一个值、该值可以通过调用recover函数进行捕获。主要的区别是,即使当前goroutine处于panic状态,或当前goroutine中存在活动紧急情况,恢复调用仍可能无法检索这些活动紧急情况抛出的值。
print、println 底层打印函数
complex、real imag 用于创建和操作复数,imag返回complex的实部,real返回complex的虚部
delete 从map中删除key对应的value

内置接口:

type error interface {
    
    
    //只要实现了Error()函数,返回值为String的都实现了err接口
    Error()    String
}

递归函数

当一个函数在其函数体内调用自身,则称之为递归。

递归是一种强有力的技术特别是在处理数据结构的过程中.

// 递归函数定义

func Processing(n int) (res int) {
    
    
	if n <= 1 {
    
    
		res = 1
	} else {
    
    
		res = Processing(n-1) + Processing(n-2)
	}
	return
}
//调用

result := 0
for i := 0; i <= 10; i++ {
    
    
    result = basic.Processing(i)
    fmt.Printf("processing(%d) is: %d\n", i, result)
}

输出结果:

processing(0) is: 1
processing(1) is: 1
processing(2) is: 2
processing(3) is: 3
processing(4) is: 5
processing(5) is: 8
processing(6) is: 13
processing(7) is: 21
processing(8) is: 34
processing(9) is: 55
processing(10) is: 89

在使用递归函数时经常会遇到的一个重要问题就是栈溢出:一般出现在大量的递归调用导致的程序栈内存分配耗尽。

这个问题可以通过一个名为懒惰求值的技术解决,在 Go 语言中,我们可以使用管道(channel)和 goroutine也会通过这个方案来优化斐波那契数列的生成问题。

这样许多问题都可以使用优雅的递归来解决,比如说著名的快速排序算法。

匿名函数

在 Go 中,匿名函数是一种没有名字的函数,通常用于简单的逻辑处理或者作为其他函数的参数。它们可以像普通函数一样使用,并且不需要提前声明或定义。

当我们不希望给函数起名字的时候,可以使用匿名函数。

匿名函数由一个不带函数名的函数声明和函数体组成. 通常是一次性函数,不希望再次使用

匿名函数结构:

func() {
    
    
    //func body
}() //花括号后加()表示函数调用,此处声明时为指定参数列表

例子

//如:
fun(a,b int) {
    
    
   fmt.Println(a+b)
}(1,2)

表示参数列表的第一对括号必须紧挨着关键字 func,因为匿名函数没有名称。

花括号 {} 涵盖着函数体,最后的一对括号表示对该匿名函数的调用。

除了可以直接对匿名函数进行调用:func(x, y int) int { return x + y } (3, 4),之外,可以被赋值于某个变量,即保存函数的地址到变量中:fn := func(x, y int) int { return x + y },然后通过变量名对函数进行调用:fn(1,2)。

总之:

在使用匿名函数时,我们可以将其直接作为函数参数传递给其他函数,或者将其赋值给变量,从而实现更加灵活的编程方式。同时,匿名函数也常用于实现简单的闭包,从而在程序中保留某些状态或者上下文信息。

闭包函数

在谈到匿名函数我们在补充下闭包函数,闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大。

Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。

匿名函数是无需定义标示符(函数名)的函数;而闭包是指能够访问自由变量的函数。

换句话说,定义在闭包中的函数可以”记忆”它被创建时候的环境。闭包函数=匿名函数+环境变量。

总的来说: 在 Go 中,闭包函数是一种特殊的匿名函数,可以访问其外部作用域中的变量,并在函数内部持久化存储它们。这使得闭包函数非常有用,可以实现很多高级编程技巧。

以下是一个使用闭包函数的示例:

func Counter() func() int {
    
    
	i := 0
	return func() int {
    
    
		i++
		return i
	}
}

闭包使用

c1 := basic.Counter()
fmt.Println(c1())
fmt.Println(c1())

c2 := basic.Counter()
fmt.Println(c2())
fmt.Println(c2())

运行的结果:

1
2
1
2

在该示例中,我们定义了一个名为 counter 的函数,它返回一个闭包函数。闭包函数内部定义了一个整数类型的变量 i,并返回一个无参数的匿名函数。这个匿名函数在每次被调用时会将 i 的值加1,并返回这个新值。由于闭包函数可以访问其外部作用域中的变量,因此它可以持久化存储变量 i 的状态。

main 函数中,我们首先通过 counter 函数创建了一个闭包函数 c1。我们连续调用 c1() 两次,并打印其结果到控制台上。这将输出 “1” 和 “2”,表示闭包函数成功地持久化存储了变量 i 的状态。

接下来,我们再次调用 counter 函数创建了一个新的闭包函数 c2。我们同样连续调用 c2() 两次,并打印其结果到控制台上。这将输出 “1” 和 “2”,但是与 c1 的结果完全独立,因为每个闭包函数都有自己的变量状态。

通过使用闭包函数,我们可以轻松地实现很多高级编程技巧,例如延迟计算、惰性求值、缓存等。需要注意的是,在使用闭包函数时,我们需要特别注意变量的生命周期和作用域,以避免出现内存泄漏和其他错误。

defer延迟函数

Go语言的defer算是一个语言的新特性,至少对比当今主流编程语言如此.

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking. defer语句调用一个函数,这个函数执行会推迟,直到外围的函数返回,或者外围函数运行到最后,或者相应的goroutine panic

defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。

通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。

释放资源的defer应该直接跟在请求资源的语句后。

f,err := os.Open(filename)
if err != nil {
    
    
    panic(err)
}
defer f.Close()

如果有多个defer表达式,调用顺序类似于栈,越后面的defer表达式越先被调用。

在处理其他资源时,也可以采用defer机制,比如对文件的操作:

package ioutil
func ReadFile(filename string) ([]byte, error) {
    
    
  f, err := os.Open(filename)
  if err != nil {
    
    
  return nil, err
}
  defer f.Close()
  return ReadAll(f)
}

也可以处理互斥锁:

var mu sync.Mutex
var m = make(map[string]int)
func lookup(key string) int {
    
    
 mu.Lock()
 defer mu.Unlock()
 return m[key]
}

调试复杂程序时,defer机制也常被用于记录何时进入和退出函数。

此外在使用defer函数的时候也会遇到些意外的情况,那就是defer使用时需要注意的坑: 先来看看几个例子。例1:

func f() (result int) {
    
    
    defer func() {
    
    
        result++
    }()
    return 0
}

例2:

func f() (r int) {
    
    
     t := 5
     defer func() {
    
    
       t = t + 5
     }()
     return t
}

例3:

func f() (r int) {
    
    
    defer func(r int) {
    
    
          r = r + 5
    }(r)
    return 1
}

自己可以先跑下看看这三个例子是不是和自己想的不一样的呢!结果确实是的例1的正确答案不是0,例2的正确答案不是10,如果例3的正确答案不是6……

defer是在return之前执行的。这个在 官方文档中是明确说明了的。

要使用defer时不踩坑,最重要的一点就是要明白,return A这一条语句并不是一条原子指令!

返回值 = A
调用defer函数
空的return

接着我们看下例1,它可以改写成这样:

func f() (result int) {
    
    
     result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() {
    
     //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}

所以例子1的这个返回值是1。

再看例2,它可以改写成这样:

func f() (r int) {
    
    
     t := 5
     r = t //赋值指令
     func() {
    
            //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令
}

所以这个的结果是5。

最后看例3,它改写后变成:

func f() (r int) {
    
    
     r = 1  //给返回值赋值
     func(r int) {
    
            //这里改的r是传值传进去的r,不会改变要返回的那个r值
          r = r + 5
     }(r)
     return        //空的return
}

所以这个例子3的结果是1

defer确实是在return之前调用的。但表现形式上却可能不像。本质原因是return A语句并不是一条原子指令,defer被插入到了赋值 与ret之间,因此可能有机会改变最终的返回值。

变量

var 语句用于声明一个变量列表。var` 语句可以出现在包或函数级别。

变量的声明的语法一般是:

var 变量名字 类型 = 表达式

通常情况下“类型”或“= 表达式”两个部分可以省略其中的一个。

例子:

// 声明并初始化一个整数类型变量 x
var x int = 10

跟函数的参数列表一样,类型在最后

在 Go 中,变量是程序中存储数据的基本单元。每个变量都有一个类型和一个值,并且可以被赋值、传递和修改。

以下是一些基本的变量定义和使用方式:

package main

import "fmt"

func main() {
    
    
    // 声明并初始化一个整数类型变量 x
    var x int = 10

    // 声明并初始化一个字符串类型变量 s
    var s string = "hello"

    // 声明一个布尔类型变量 b,不需要显式初始化,默认为 false
    var b bool

    // 打印变量的值
    fmt.Println(x)
    fmt.Println(s)
    fmt.Println(b)

    // 修改变量的值
    x = 20
    s = "world"
    b = true

    // 再次打印变量的值
    fmt.Println(x)
    fmt.Println(s)
    fmt.Println(b)

    // 短变量声明语法
    y := 30
    z, w := "foo", true

    // 打印新的变量
    fmt.Println(y)
    fmt.Println(z)
    fmt.Println(w)

    // 类型推导
    m := 40
    n := "bar"

    // 打印新的变量
    fmt.Println(m)
    fmt.Println(n)
}

在该示例中,我们首先使用 var 关键字声明和初始化了三个变量:xsb

其中,x 是一个整数类型变量,初始化为 10s 是一个字符串类型变量,初始化为 “hello”;b 是一个布尔类型变量,没有显式初始化,因此默认值为 false

我们通过 fmt.Println 函数打印了这三个变量的值,并修改了它们的值。

示例接下来,演示了 Go 中的短变量声明语法和类型推导方式。

使用短变量声明语法可以更简洁地定义和初始化变量,而类型推导则可以让编译器自动推断变量类型,避免冗长的类型声明。

需要注意的是,在使用变量时,我们需要特别注意变量的作用域和生命周期,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。

变量的初始化

变量声明可以包含初始值,每个变量对应一个。

如果初始化值已存在,则可以省略类型;变量会从初始值中获得类型。

var i, j int = 1, 2
func Func() {
    
    
    var c, python, java = true, false, "no!"
    fmt.Println(i, j, c, python, java)
}

短变量声明

在函数中,简洁赋值语句 := 可在类型明确的地方代替 var 声明。

函数外的每个语句都必须以关键字开始(var, func 等等),因此 := 结构不能在函数外使用。

import "fmt"
func Func() {
    
    
    var i, j int = 1, 2
    k := 3
    c, python, java := true, false, "no!"
    fmt.Println(i, j, k, c, python, java)
}

零值

没有明确初始值的变量声明会被赋予它们的 零值

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。
func Func() {
    
    
    var i int
    var f float64
    var b bool
    var s string
    fmt.Printf("%v %v %v %q\n", i, f, b, s)
}

常量

在 Go 中,常量是一种固定不变的值,其值在编译时就已经确定,不能被修改。常量通常用于存储程序中不可变的值,例如数学常数、密码等。

以下是一些基本的常量定义和使用方式:

package main

import "fmt"

func main() {
    
    
    // 声明一个整数类型常量
    const x int = 10

    // 声明一个字符串类型常量
    const s string = "hello"

    // 打印常量的值
    fmt.Println(x)
    fmt.Println(s)

    // 尝试修改常量的值,会导致编译错误
    // x = 20
    // s = "world"
}

在该示例中,我们使用 const 关键字定义了两个常量:xs。其中,x 是一个整数类型常量,初始化为 10s 是一个字符串类型常量,初始化为 “hello”。我们通过 fmt.Println 函数打印了这两个常量的值,并尝试修改它们的值,结果会导致编译错误。

需要注意的是,在使用常量时,我们需要特别注意常量的作用域和生命周期,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。

数值常量

数值常量是高精度的 。一个未指定类型的常量由上下文来决定其类型。

在 Go 中,数值常量是一种固定不变的数值,其值在编译时就已经确定,不能被修改。数值常量可以使用各种进制和精度表示。

以下是一些基本的数值常量定义和使用方式:

package main

import "fmt"

func main() {
    
    
    // 十进制表示整数常量
    const x int = 10

    // 八进制表示整数常量
    const y int = 012

    // 十六进制表示整数常量
    const z int = 0x1a

    // 浮点数常量
    const a float64 = 3.14

    // 复数常量
    const b complex128 = 1 + 2i

    // 打印常量的值
    fmt.Println(x)
    fmt.Println(y)
    fmt.Println(z)
    fmt.Println(a)
    fmt.Println(b)

    // 尝试修改常量的值,会导致编译错误
    // x = 20
    // y = 0123
    // z = 0x1b
    // a = 3.15
    // b = 2 + 1i
}

在该示例中,我们使用不同的进制和精度表示了各种类型的数值常量,包括整数常量、浮点数常量和复数常量。我们通过 fmt.Println 函数打印了这些常量的值,并尝试修改它们的值,结果会导致编译错误。

需要注意的是,在使用数值常量时,我们需要特别注意常量的精度和范围,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。

GO数据基本类型

Go 的数据类型分四大类:

  1. 基本类型: 数字 number,字符串 string 和布尔型 boolean。
  2. 聚合类型: 数组 array 和构造体 struct。
  3. 援用类型: 指针 pointer,切片 slice,字典 map,函数 func 和通道 channel。
  4. 接口类型: 接口 interface。

其中,基本类型又分为:

  1. 整型: int8、uint8、byte、int16、uint16、int32、uint32、int64、uint64、int、uint、uintptr。
  2. 浮点型: float32,float64。
  3. 复数类型: complex64、complex128。
  4. 布尔型: bool。
  5. 字符串: string。
  6. 字符型: rune。

常用的 Go 的基本类型有

bool
 
string
 
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr
 
byte // uint8 的别名
 
rune // int32 的别名
    // 表示一个 Unicode 码点
 
float32 float64
 
complex64 complex128

本例展示了几种类型的变量。

同导入语句一样,变量声明也可以“分组”成一个语法块。

int, uintuintptr 在 32 位系统上通常为 32 位宽,在 64 位系统上则为 64 位宽。 当你需要一个整数值时应使用 int 类型,除非你有特殊的理由使用固定大小或无符号的整数类型。

basic-types.go

package basic

import (
	"fmt"
	"math/cmplx"
)

var (
	ToBe   bool       = false
	MaxInt uint64     = 1<<64 - 1
	z      complex128 = cmplx.Sqrt(-5 + 12i)
)

func BasicTypeDemo() {
    
    
	fmt.Printf("Type: %T Value: %v\n", ToBe, ToBe)
	fmt.Printf("Type: %T Value: %v\n", MaxInt, MaxInt)
	fmt.Printf("Type: %T Value: %v\n", z, z)
}

类型转换

表达式 T(v) 将值 v 转换为类型 T

一些关于数值的转换:

var i int = 42
var f float64 = float64(i)
var u uint = uint(f)

或者,更加简单的形式:

i := 42
f := float64(i)
u := uint(f)

与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换。试着移除例子中 float64uint 的转换看看会发生什么。

类型推导

在声明一个变量而不指定其类型时(即使用不带类型的 := 语法或 var = 表达式语法),变量的类型由右值推导得出。

当右值声明了类型时,新变量的类型与其相同:

var i int
j := i // j 也是一个 int

不过当右边包含未指明类型的数值常量时,新变量的类型就可能是 int, float64complex128 了,这取决于常量的精度:

i := 42           // int
f := 3.142        // float64
g := 0.867 + 0.5i // complex128

尝试修改示例代码中 v 的初始值,并观察它是如何影响类型的。

整型

Go语言同时提供了有符号和无符号类型的整数运算。

有符号整形数类型:

int8,长度:1字节, 取值范围:(-128 ~ 127)
int16,长度:2字节,取值范围:(-32768 ~ 32767int32,长度:4字节,取值范围:(-2,147,483,648 ~ 2,147,483,647int64.长度:8字节,取值范围:(-9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807)

无符号整形数类型:

uint8,长度:1字节, 取值范围:(0 ~ 255)
uint16,长度:2字节,取值范围:(0 ~ 65535)
uint32,长度:4字节,取值范围:(0 ~ 4,294,967,295)
uint64.长度:8字节,取值范围:(0 ~ 18,446,744,073,709,551,615)

byte是uint8类型的等价类型,byte类型一般用于强调数值是一个原始的数据而不是 一个小的整数。

uintptr 是一种无符号的整数类型,没有指定具体的bit大小但是足以容纳指针。

uintptr类型只有在底层编程是才需要,特别是Go语言和C语言函数库或操作系统接口相交互的地方。

此外在这里还需要了解下进制的转换方便以后学习和使用:

十进制整数: 使用0-9的数字表示且不以0开头。// 100 123455
八进制整数:0开头,0-7的数字表示。 // 0100 0600
十六进制整数:0X或者是0x开头,0-9|A-F|a-f组成 //0xff 0xFF12

浮点型

浮点型。float32 精确到小数点后 7 位,float64 精确到小数点后 15 位。

由于精确度的缘故,你在使用 == 或者 != 来比较浮点数时应当非常小心。

浮点型(IEEE-754 标准):
float32:+- 1e-45 -> +- 3.4 * 1e3832位浮点类型
float64:+- 5 1e-324 -> 107 1e30864位浮点类型

浮点型中指数部分由”E”或”e”以及带正负号的10进制整数表示。例:3.9E-2表示浮点数0.039。3.9E+1表示浮点数39。 有时候浮点数类型值也可以被简化。比如39.0可以被简化为39。0.039可以被简化为.039。在Golang中浮点数的相关部分只能由10进制表示法表示。

复数

复数类型:
complex64: 由两个float32类型的值分别表示复数的实数部分和虚数部分
complex128: 由两个float64类型的值表示复数的实数部分和虚数部分

复数类型的值一般由浮点数表示的实数部分、加号”+”、浮点数表示的虚数部分以及小写字母”i”组成,

例如:

var x complex128 = complex(1,2)  //1+2i

对于一个复数 c = complex(x, y) ,可以通过Go语言内置函数 real(z) 获得该复数的实 部,也就是 x ,通过 imag© 获得该复数的虚部,也就是 y 。

布尔型

在Go语言中,布尔值的类型为 bool,值是 true 或 false,布尔可以做3种逻辑运算,&&(逻辑且),||(逻辑或),!(逻辑非),布尔类型的值不支持其他类型的转换.

布尔值可以和&&(AND)和||(OR)操作符结合,并且可能会有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不在被求值,因此下面的表达式总是安全的:

 s != "" && s[0] == 'x'

其中s[0]操作如果应用于空字符串将会导致panic异常。

rune类型

字符是 UTF-8 编码的 Unicode 字符,Unicode 为每一个字符而非字形定义唯一的码值(即一个整数),

例如 字符a 在 unicode 字符表是第 97 个字符,所以其对应的数值就是 97,

也就是说对于Go语言处理字符时,97 和 a 都是指的是字符a,而 Go 语言将使用数值指代字符时,将这样的数值称呼为 rune 类型。

rune类型是 Unicode 字符类型,和 int32 类型等价,通常用于表示一个 Unicode 码点。

rune 和 int32 可以互换使用。 一个Unicode代码点通常由”U+”和一个以十六进制表示法表示的整数表示,例如英文字母’A’的Unicode代码点为”U+0041”。

在 Go 中,rune 类型是用于表示 Unicode 码点的类型。Unicode 是一种标准,用于为世界上各种语言和符号分配唯一的数字编码,以便它们可以在计算机中存储和处理。

在 Go 中,rune 类型实际上是一个 int32 类型的别名。它可以用于表示任何 Unicode 码点,并提供了一些有用的函数和方法,用于处理字符串和 Unicode 编码。

以下是一些基本的 rune 类型使用方式:

func RuneDemo() {
    
    
	// 使用单引号表示一个 rune 类型值
	var r rune = '你'

	// 打印这个 rune 类型值
	fmt.Println(r)

	// 将 rune 类型转换为字符串类型
	s := string(r)
	fmt.Println(s)

	// 遍历一个字符串并打印每个字符的 Unicode 码点
	for i, c := range "hello 世界" {
    
    
		fmt.Printf("字符 %d: %U\n", i, c)
	}
}

在该示例中:

  • 我们首先使用单引号表示了一个 rune 类型值,即 Unicode 码点 “你”。我们通过 fmt.Println 函数打印了这个 rune 类型值,并将其转换为字符串类型。
  • 接下来,我们遍历了一个字符串,并通过 %U 格式化符号打印了每个字符的 Unicode 码点。

输出的结果如下:

20320
字符 0: U+0068
字符 1: U+0065
字符 2: U+006C
字符 3: U+006C
字符 4: U+006F
字符 5: U+0020
字符 6: U+4E16
字符 9: U+754C

需要注意的是,在处理字符串和 Unicode 编码时,我们需要特别注意编码和解码的方式,以避免出现错误或者不正确的结果。同时,我们也要了解各种字符集和编码标准之间的差异,以便在处理多语言和多文化环境下的应用程序时保持最佳实践。

此外rune类型的值需要由单引号”‘“包裹,不过我们还可以用另外几种方式表示:

Integer - Figure 1

rune类型值的表示中支持几种特殊的字符序列,即:转义符。

Integer - Figure 2

字符串

在Go语言中,组成字符串的最小单位是字符,存储的最小单位是字节,字符串本身不支持修改。

字节是数据存储的最小单元,每个字节的数据都可以用整数表示,例如一个字节储存的字符a,实际存储的是97而非字符的字形,将这个实际存储的内容用数字表示的类型,称之为byte。

字符串是不可变的字节序列,它可以包含任意数据,包括0值字节,但是主要还是为了人可读的文本。内置的 len()函数返回字符串的字节数。

字符串的表示法有两种,即:原生表示法和解释型表示法。原生表示法,需用用反引号”`”把字符序列包起来,如果用解释型表示法,则需要用双引号”””包裹字符序列。

var str1 string = "keke"
var str2 string = `keke`

这两种表示的区别是,前者表示的是所见即所得的(除了回车符)。后者所表示的值中转义符会起作用。字符串值是不可变的,如果我们创建了一个此类型的值,就不可能再对它本身做任何修改。

var str string  // 声明一个字符串变量
str = "hai keke" // 字符串赋值
ch := str[0] // 取字符串的第一个字符

整型运算

在整型运算中,算术运算、逻辑运算和比较运算,运算符优先级从上到下递减顺序排列:

 *      /     %     <<     >>     &     &^ 
 +      -     |     ^      
 ==     !=    <     <=     >      >=
 &&
 ||

在同一个优先级,使用左优先结合规则,但是使用括号可以明确优先顺序。

bit位操作运算符:

符号 操作 操作数是否区分符号
& 位运算 AND No
^ 位运算 XOR No
&^ 位清空 (AND NOT) No
<< 左移 Yes
>> 右移 Yes

复合数据类型

复合数据类型主要有:

  • 数组
  • Slice
  • Map
  • 结构体
  • JSON

数组和结构体都是有固定内存大小的数据结构。

在复合数据类型中数组是由同构的元素组成——每个数组元素都是完全相同的类型——结构体则是由异构的元素组成的。

相比之下,slice和map则是动态的数据结构,它们将根据需要动态增长。

数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。

在 Go 中,数组是一种固定长度、类型相同的数据结构。数组可以包含任何类型的元素,但是它们的长度在定义时就必须确定,并且无法动态改变。

一个数组的案例

以下是一个基本的数组定义和使用方式:

package main

import "fmt"

func ArrayDemo() {
    
    
    // 定义一个包含五个整数的数组
    var a [5]int

    // 打印数组的值
    fmt.Println(a)

    // 修改数组中的元素
    a[2] = 3
    fmt.Println(a)
    fmt.Println(a[2])

    // 定义并初始化一个数组
    b := [3]string{
    
    "hello", "world", "!"}
    fmt.Println(b)

    // 遍历数组并打印每个元素
    for i, x := range b {
    
    
        fmt.Printf("b[%d]: %s\n", i, x)
    }
}

执行结果如下:

[0 0 0 0 0]
[0 0 3 0 0]
3
[hello world !]
b[0]: hello
b[1]: world
b[2]: !

在该示例中,我们首先使用 var 关键字定义了一个包含五个整数的数组 a。由于没有显式初始化数组中的元素,因此它们都被自动初始化为零值。

我们通过 fmt.Println 函数打印了这个数组的值,并修改了其中的一个元素,并再次打印了这个数组的值和一个特定的元素。

接下来,我们使用短变量声明语法定义了一个包含三个字符串的数组 b,并通过花括号进行了初始化。我们遍历了数组 b 并通过 fmt.Printf 函数打印了每个元素的索引和值。

需要注意的是,在使用数组时,我们需要特别注意数组的长度和元素类型,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。同时,我们也要注意数组在内存中的分布和访问方式,以便在处理大型数据集和高性能应用程序时保持最佳性能。

数组的定义

数组的每个元素都被初始化为元素类型对应的零值,对于数字类型来说就是0。

var m [3]int = [3]int{
    
    1, 2, 3}
var n [3]int = [3]int{
    
    1, 2}
fmt.Println(n[2]) // "0"

在数组字面值中,如果在数组的长度位置出现的是“…”省略号,则表示数组的长度是根据初始化值的 个数来计算。

m := [...]int{
    
    1, 2, 3}
fmt.Printf("%T\n", m) // "[3]int"

数组的长度是数组类型的一个组成部分,因此[3]int和[4]int是两种不同的数组类型。数组的长度必须是常量表达式,因为数组的长度需要在编译阶段确定。

数组可以直接进行比较,当数组内的元素都一样的时候表示两个数组相等。

arr1 := [3]int{
    
    1, 2, 3}
arr2 := [3]int{
    
    1, 2, 3}
arr3 := [3]int{
    
    1, 2, 4} 
fmt.Println(arr1 == arr2, arr1 == arr3)  //true,false

元素的访问

数组的每个元素可以通过索引下标来访问,索引下标的范围是从0开始到数组长度减1的位置。内置的len函数将返回数组中元素的个数。

数组作为参数

数组可以作为函数的参数传入,但由于数组在作为参数的时候,其实是进行了拷贝,这样在函数内部改变数组的值,是不影响到外面的数组的值。

func ArrIsArgs(arr [4]int) {
    
    
    arr[0] = 120
}
m := [...]int{
    
    1, 2, 3, 4}
ArrIsArgs(m)

如果想要改变外部数组的值,就只能使用指针,

使用指针,在函数内部改变的数组的值,也会改变外面的数组的值:

func ArrIsArgs(arr *[4]int) {
    
    
   arr[0] = 20
}
m:= [...]int{
    
    1, 2, 3, 4}
ArrIsArgs(&m)

这里的* 和&的区别:

  • & 是取地址符号 , 即取得某个变量的地址 , 如 ; &a
  • *是指针类型变量定义 , 可以表示一个变量是指针类型 , 也可以表示一个指针变量所指向的存储单元 , 也就是这个地址所存储的值.

通常这样的情况下都是用切片来解决,而不是用数组。

由于数组的长度是固定的,因而在使用的时候我们用的最多的是slice(切片),它是可以增长和收缩动态序列,slice功能也更灵活。

Slice

Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。一个slice类型一般写作[]T,其中T代表slice中元素的类型;slice的语法和数组很像,只是没有固定长度而已。

数组和slice关系非常密切,一个slice可以访问数组的部分或者全部数据,而且slice的底层本身就是对数组的引用。

从数组或切片生成新的切片

切片默认指向一段连续内存区域,可以是数组,也可以是切片本身。从连续内存区域生成切片是常见的操作,格式如下:

// slice:表示目标切片对象
// 开始位置:对应目标切片对象的索引
// 结束位置:对应目标切片的结束索引
slice [开始位置 : 结束位置]

一个Slice由三部分组成:指针,长度和容量。

  • 内置的len和cap函数可以分别返回slice的长度和容量。
  • 指针指向第一个slice元素对应的底层数组元素的地址,要注意的是slice的第一个元素并不一定就是数组的第一个元素。
  • 长度对应slice中元素的数目;长度不能超过容量,容量一般是从slice的开始位置到底层数据的结尾位置。内置的len和cap函数分别返回slice的长度和容量。

一个Slice的案例

在 Go 中,切片是一种动态长度的、可变长数组数据结构。它基于数组实现,但是可以动态增加或减少其长度,并且支持各种强大的操作和函数。

以下是一个基本的切片定义和使用方式:


func SliceDemo() {
    
    
    // 定义一个包含五个整数的数组
    a := [5]int{
    
    1, 2, 3, 4, 5}

    // 声明一个从数组 a 中获取的切片
    s := a[1:4]

    // 打印切片的值和长度
    fmt.Println(s)
    fmt.Println(len(s))

    // 修改切片中的元素
    s[1] = 10
    fmt.Println(s)
    fmt.Println(a)

    // 使用 make 函数创建一个新的切片
    b := make([]int, 3)
    fmt.Println(b)

    // 向切片中添加新的元素
    b = append(b, 4, 5, 6)
    fmt.Println(b)

    // 遍历切片并打印每个元素
    for i, x := range b {
    
    
        fmt.Printf("b[%d]: %d\n", i, x)
    }
}

执行结果

[2 3 4]
3
[2 10 4]
[1 2 10 4 5]
[0 0 0]
[0 0 0 4 5 6]
b[0]: 0
b[1]: 0
b[2]: 0
b[3]: 4
b[4]: 5
b[5]: 6

在该示例中,我们首先定义了一个包含五个整数的数组 a,并通过 [1:4] 的方式声明了一个从数组 a 中获取的切片 s。我们通过 fmt.Println 函数打印了这个切片的值和长度,并修改了其中的一个元素,查看了其对原数组的影响。

接下来,我们使用 make 函数创建了一个新的切片 b,并通过 append 函数向其中添加了三个新元素。我们遍历了切片 b 并通过 fmt.Printf 函数打印了每个元素的索引和值。

需要注意的是,在使用切片时,我们需要特别注意其底层数组和长度,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。

同时,我们也要掌握切片的内存分配和释放方式,以便在处理大型数据集和高性能应用程序时保持最佳性能。

从指定范围中生成切片

切片有点像C语言里的指针,指针可以做运算,但代价是内存操作越界

切片在指针的基础上增加了大小,约束了切片对应的内存区域,切片使用中无法对切片内部的地址和大小进行手动调整,因此切片比指针更安全、强大

切片和数组密不可分,如果将数组理解为一栋办公楼,那么切片就是把不同的连续楼层出租给使用者,出租的过程需要选择开始楼层和结束楼层,这个过程就会生成切片,表示原有的切片

生成切片的格式中,当开始和结束位置都被忽略时,生成的切片将表示和原切片一致的切片,并且生成的切片与原切片在数据内容上也是一致的,代码如下:

a := []int{
    
    1, 2, 3}
fmt.Println(a[:])

a 是一个拥有 3 个元素的切片,将 a 切片使用 a[:] 进行操作后,得到的切片与 a 切片一致,代码输出如下:

[1 2 3]

从数组生成切片,代码如下:

var a  = [3]int{
    
    1, 2, 3}
fmt.Println(a, a[1:2])

其中 a 是一个拥有 3 个整型元素的数组,被初始化为数值 1 到 3,使用 a[1:2] 可以生成一个新的切片,代码运行结果如下:

[1 2 3]  [2]

其中 [2] 就是 a[1:2] 切片操作的结果。

从数组或切片生成新的切片拥有如下特性:

  • 取出的元素数量为:结束位置 - 开始位置;
  • 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
  • 当缺少开始位置时,表示从连续区域开头到结束位置;
  • 当缺少结束位置时,表示从开始位置到整个连续区域末尾;
  • 两者同时缺少时,与切片本身等效;
  • 两者同时为 0 时,等效于空切片,一般用于切片复位。

根据索引位置取切片 slice 元素值时,取值范围是(0~len(slice)-1),超界会报运行时错误,生成切片时,结束位置可以填写 len(slice) 但不会报错。下面通过实例来熟悉切片的特性:

重置切片,清空拥有的元素

把切片的开始和结束位置都设为 0 时,生成的切片将变空,代码如下:

a := []int{
    
    1, 2, 3}
fmt.Println(a[0:0])

代码输出如下:

[]

直接声明新的切片

除了可以从原有的数组或者切片中生成切片外,也可以声明一个新的切片,每一种类型都可以拥有其切片类型,表示多个相同类型元素的连续集合,因此切片类型也可以被声明,切片类型声明格式如下:

// 其中 name 表示切片的变量名
// Type 表示切片对应的元素类型
var name []Type

下面代码展示了切片声明的使用过程:

// 声明字符串切片
// 声明一个字符串切片,切片中拥有多个字符串
var strList []string

// 声明整型切片
// 声明一个整型切片,切片中拥有多个整型数值
var numList []int

// 声明一个空切片
// 将 numListEmpty 声明为一个整型切片
// 本来会在{}中填充切片的初始化元素,这里没有填充,所以切片是空的,但是此时的 numListEmpty 已经被分配了内存,只是还没有元素
var numListEmpty = []int{
    
    }

// 输出3个切片
// 切片均没有任何元素,3 个切片输出元素内容均为空
fmt.Println(strList, numList, numListEmpty)

// 输出3个切片大小
// 没有对切片进行任何操作,strList 和 numList 没有指向任何数组或者其他切片
fmt.Println(len(strList), len(numList), len(numListEmpty))

// 切片判定空的结果
//声明但未使用的切片的默认值是 nil,strList 和 numList 也是 nil,所以和 nil 比较的结果是 true
// numListEmpty 已经被分配到了内存,但没有元素,因此和 nil 比较时是 false
fmt.Println(strList == nil)
fmt.Println(numList == nil)
fmt.Println(numListEmpty == nil)

代码输出结果:

[] [] []
0 0 0
true
true
false

共享底层的数据

多个slice之间可以共享底层的数据,并且引用的数组部分区间可能重叠。

下面有一个月份的数组:

months := [...]string{
    
    1: "January", /* ... */, 12: "December"}

一月份是months[1],十二月份是months[12]。

通常,数组的第一个元素从索引0开始,但是月份一般是从1开始的,因此我们声明数组时直接跳过第0个元素,第0个元素会被自动初始化为空字符串。

下图显示了表示一 年中每个月份名字的字符串数组,还有重叠引用了该数组的两个slice。数组这样定义.

Slice - Figure 1

slice的切片操作s[i:j],其中0 ≤ i≤ j≤ cap(s),用于创建一个新的slice,引用s的从第i个元素开始到第j-1个元素的子序列。

新的slice将只有j-i个元素。如果i位置的索引被省略的话将使用0代替,如果j位置的索引被省略的话将使用len(s)代替。

如果切片操作超出cap(s)的上限将导致一个panic异常,但是超出len(s)则是意味着扩展了lice,因为新slice的长度会变大.

切片Slice的长度len与容量cap

切片拥有长度容量
len长度是它所包含的元素个数
cap容量是从它的第一个元素开始数,到其底层数组元素末尾的个数
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

package main

import "fmt"

func main() {
    
    
	s := []int{
    
    2, 3, 5, 7, 11, 13}
	printSlice(s)

	// 截取切片使其长度为 0
	s = s[:0]
	printSlice(s)

	// 拓展其长度
	s = s[:4]
	printSlice(s)

	// 舍弃前两个值
	s = s[2:]
	printSlice(s)
}

func printSlice(s []int) {
    
    
	fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}

运行结果

len=6 cap=6 [2 3 5 7 11 13]
len=0 cap=6 []
len=4 cap=6 [2 3 5 7]
len=2 cap=4 [5 7]

slice创建方式

slice创建方式主要有两种:1.基于数组创建。2.直接创建

  • 基于数组创建:
arrVar := [4]int{
    
    1, 2, 34}
sliceVar := arrVar[1:3]

数组arrVar和sliceVar里面的地址其实是一样的,因而如果你改变sliceVar里面的变量,那么arrVar里面的变量也会随之改变。

  • 直接创建

内建函数new分配了零值填充的元素类型的内存空间,并且返回其地址,一个指针类型的值。

var p *[]int = new([]int)  //分配slice结构内存
var[]int = make([]int,100) //m指向一个新分配的有100个整数的数组

new 分配;make 初始化,, 因此:

new(T) 返回 *T 指向一个零值 T
make(T) 返回初始化后的 T

注意: make仅适用于 map,slice 和 channel,并且返回的不是指针。应当用 new 获得特定的指针。

内置的make函数创建一个指定元素类型、长度和容量的slice。容量部分可以省略,在这种情况下,容量将等于长度。

使用内置的make()函数来创建。事实上还是会创建一个匿名的数组,只是不需要我们来定义

使用 make() 函数构造切片

如果需要动态地创建一个切片,可以使用 make() 内建函数,格式如下:

// 其中 Type 是指切片的元素类型
// size 指的是为这个类型分配多少个元素
// cap 为预分配的元素数量,这个值设定后不影响 size,只是能提前分配空间,降低多次分配空间造成的性能问题
make( []Type, size, cap )

示例如下:

a := make([]int, 2)
b := make([]int, 2, 10)

fmt.Println(a, b)
fmt.Println(len(a), len(b))

代码输出如下:

[0 0] [0 0]
2 2

其中 a 和 b 均是预分配 2 个元素的切片,只是 b 的内部存储空间已经分配了 10 个,但实际使用了 2 个元素。

容量不会影响当前的元素个数,因此 a 和 b 取 len 都是 2。

温馨提示:使用 make() 函数生成的切片一定发生了内存分配操作,但给定开始与结束位置(包括切片复位)的切片只是将新的切片结构指向已经分配好的内存区域,设定开始与结束位置,不会发生内存分配操作。

append()为切片添加元素

Go语言的内建函数 append() 可以为切片动态添加元素,代码如下所示:

var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{
    
    1,2,3}...) // 追加一个切片, 切片需要解包

不过需要注意的是,在使用 append() 函数为切片动态添加元素时,如果空间不足以容纳足够多的元素,切片就会进行“扩容”,此时新切片的长度会发生改变。

切片在扩容时,容量的扩展规律是按容量的 2 倍数进行扩充,例如 1、2、4、8、16……,代码如下:

// 声明一个整型切片
var numbers []int

// 循环向 numbers 切片中添加 10 个数
for i := 0; i < 10; i++ {
    
    
    numbers = append(numbers, i)
    // 打印输出切片的长度、容量和指针变化,使用函数 len() 查看切片拥有的元素个数,使用函数 cap() 查看切片的容量情况
    fmt.Printf("len: %d  cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
}

代码输出如下:

len: 1  cap: 1 pointer: 0xc0420080e8
len: 2  cap: 2 pointer: 0xc042008150
len: 3  cap: 4 pointer: 0xc04200e320
len: 4  cap: 4 pointer: 0xc04200e320
len: 5  cap: 8 pointer: 0xc04200c200
len: 6  cap: 8 pointer: 0xc04200c200
len: 7  cap: 8 pointer: 0xc04200c200
len: 8  cap: 8 pointer: 0xc04200c200
len: 9  cap: 16 pointer: 0xc042074000
len: 10  cap: 16 pointer: 0xc042074000

通过查看代码输出,可以发现一个有意思的规律:切片长度 len 并不等于切片的容量 cap。

往一个切片中不断添加元素的过程,类似于公司搬家,公司发展初期,资金紧张,人员很少,所以只需要很小的房间即可容纳所有的员工,随着业务的拓展和收入的增加就需要扩充工位,但是办公地的大小是固定的,无法改变,因此公司只能选择搬家,每次搬家就需要将所有的人员转移到新的办公点。

  • 员工和工位就是切片中的元素。
  • 办公地就是分配好的内存。
  • 搬家就是重新分配内存。
  • 无论搬多少次家,公司名称始终不会变,代表外部使用切片的变量名不会修改。
  • 由于搬家后地址发生变化,因此内存“地址”也会有修改。

除了在切片的尾部追加,我们还可以在切片的开头添加元素:

var a = []int{
    
    1,2,3}
a = append([]int{
    
    0}, a...) // 在开头添加1个元素
a = append([]int{
    
    -3,-2,-1}, a...) // 在开头添加1个切片

在切片开头添加元素一般都会导致内存的重新分配,而且会导致已有元素全部被复制 1 次,因此,从切片的开头添加元素的性能要比从尾部追加元素的性能差很多。

因为 append 函数返回新切片的特性,所以切片也支持链式操作,我们可以将多个 append 操作组合起来,实现在切片中间插入元素:

var a []int
a = append(a[:i], append([]int{
    
    x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{
    
    1,2,3}, a[i:]...)...) // 在第i个位置插入切片

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a[i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a[:i] 中。

切片复制(切片拷贝)

Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。copy() 函数的使用格式如下:

// 其中 srcSlice 为数据来源切片
// destSlice 为复制的目标(也就是将 srcSlice 复制到 destSlice)
// 目标切片必须分配过空间且足够承载复制的元素个数,并且来源和目标的类型必须一致
// copy() 函数的返回值表示实际发生复制的元素个数。

copy( destSlice, srcSlice []T) int

下面的代码展示了使用 copy() 函数将一个切片复制到另一个切片的过程:

slice1 := []int{
    
    1, 2, 3, 4, 5}
slice2 := []int{
    
    5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

虽然通过循环复制切片元素更直接,不过内置的 copy() 函数使用起来更加方便,copy() 函数的第一个参数是要复制的目标 slice,第二个参数是源 slice,两个 slice 可以共享同一个底层数组,甚至有重叠也没有问题。

下面通过代码演示对切片的引用和复制操作后对切片元素的影响:

package main

import "fmt"

func main() {
    
    

    // 设置元素数量为1000
    const elementCount = 1000

    // 预分配足够多的元素切片
    // 预分配拥有 1000 个元素的整型切片,这个切片将作为原始数据
    srcData := make([]int, elementCount)

    // 将切片赋值
    // 将 srcData 填充 0~999 的整型值
    for i := 0; i < elementCount; i++ {
    
    
        srcData[i] = i
    }

    // 引用切片数据
    // 将 refData 引用 srcData,切片不会因为等号操作进行元素的复制
    refData := srcData

    // 预分配足够多的元素切片
    // 预分配与 srcData 等大(大小相等)、同类型的切片 copyData
    copyData := make([]int, elementCount)
    // 将数据复制到新的切片空间中
    // 使用 copy() 函数将原始数据复制到 copyData 切片空间中
    copy(copyData, srcData)

    // 修改原始数据的第一个元素
    // 修改原始数据的第一个元素为 999
    srcData[0] = 999

    // 打印引用切片的第一个元素
    // 引用数据的第一个元素将会发生变化
    fmt.Println(refData[0])

    // 打印复制切片的第一个和最后一个元素
    // 打印复制数据的首位数据,由于数据是复制的,因此不会发生变化
    fmt.Println(copyData[0], copyData[elementCount-1])

    // 复制原始数据从4到6(不包含)
    // 将 srcData 的局部数据复制到 copyData 中
    copy(copyData, srcData[4:6])

	// 打印复制局部数据后的 copyData 元素
    for i := 0; i < 5; i++ {
    
    
        fmt.Printf("%d ", copyData[i])
    }
}

Go语言从切片中删除元素

Go语言并没有对删除切片元素提供专用的语法或者接口,需要使用切片本身的特性来删除元素,根据要删除元素的位置有三种情况,分别是从开头位置删除、从中间位置删除和从尾部删除,其中删除切片尾部的元素速度最快。

从开头位置删除

删除开头的元素可以直接移动数据指针:

a = []int{
    
    1, 2, 3}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素

也可以不移动数据指针,但是将后面的数据向开头移动,可以用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化):

a = []int{
    
    1, 2, 3}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素

还可以用 copy() 函数来删除开头的元素:

a = []int{
    
    1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素

从中间位置删除

对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:

a = []int{
    
    1, 2, 3, ...}
a = append(a[:i], a[i+1:]...) // 删除中间1个元素
a = append(a[:i], a[i+N:]...) // 删除中间N个元素
a = a[:i+copy(a[i:], a[i+1:])] // 删除中间1个元素
a = a[:i+copy(a[i:], a[i+N:])] // 删除中间N个元素

从尾部删除

a = []int{
    
    1, 2, 3}
a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素

删除开头的元素和删除尾部的元素都可以认为是删除中间元素操作的特殊情况,下面来看一个示例:删除切片指定位置的元素,

package main

import "fmt"

func main() {
    
    
	// 声明一个整型切片,保存含有从 a 到 e 的字符串
    seq := []string{
    
    "a", "b", "c", "d", "e"}

    // 指定删除位置
    // 为了演示和讲解方便,使用 index 变量保存需要删除的元素位置
    index := 2

    // 查看删除位置之前的元素和之后的元素
    // seq[:index] 表示的就是被删除元素的前半部分,值为 [1 2]
    // seq[index+1:] 表示的是被删除元素的后半部分,值为 [4 5]
    fmt.Println(seq[:index], seq[index+1:])

    // 将删除点前后的元素连接起来
    // 使用 append() 函数将两个切片连接起来
    seq = append(seq[:index], seq[index+1:]...)
	
	// 输出连接好的新切片,此时,索引为 2 的元素已经被删除
    fmt.Println(seq)
}

代码输出结果:

[a b] [d e]
[a b d e]

提示:连续容器的元素删除无论在任何语言中,都要将删除点前后的元素移动到新的位置,随着元素的增加,这个过程将会变得极为耗时,因此,当业务需要大量、频繁地从一个切片中删除元素时,如果对性能要求较高的话,就需要考虑更换其他的容器了(如双链表等能快速从删除点删除元素)。

range循环迭代切片

通过前面的学习我们了解到切片其实就是多个相同类型元素的连续集合,既然切片是一个集合,那么我们就可以迭代其中的元素,Go语言有个特殊的关键字 range,它可以配合关键字 for 来迭代切片里的每一个元素,如下所示:

// 创建一个整型切片,并赋值
slice := []int{
    
    10, 20, 30, 40}
// 迭代每一个元素,并显示其值
// index 和 value 分别用来接收 range 关键字返回的切片中每个元素的索引和值
// 这里的 index 和 value 不是固定的,读者也可以定义成其它的名字
for index, value := range slice {
    
    
    fmt.Printf("Index: %d Value: %d\n", index, value)
}

上面代码的输出结果为:

Index: 0 Value: 10
Index: 1 Value: 20
Index: 2 Value: 30
Index: 3 Value: 40

当迭代切片时,关键字 range 会返回两个值,第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本,需要强调的是,range 返回的是每个元素的副本,而不是直接返回对该元素的引用

range 提供了每个元素的副本
// 创建一个整型切片,并赋值
slice := []int{
    
    10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
    
    
    fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
}

输出结果为:

Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

因为迭代返回的变量是一个在迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的,要想获取每个元素的地址,需要使用切片变量和索引值(例如上面代码中的 &slice[index])。

如果不需要索引值,也可以使用下划线_来忽略这个值,代码如下所示:

// 创建一个整型切片,并赋值
slice := []int{
    
    10, 20, 30, 40}
// 迭代每个元素,并显示其值
for _, value := range slice {
    
    
    fmt.Printf("Value: %d\n", value)
}
使用空白标识符(下划线)来忽略索引值
// 创建一个整型切片,并赋值
slice := []int{
    
    10, 20, 30, 40}
// 迭代每个元素,并显示其值
for _, value := range slice {
    
    
    fmt.Printf("Value: %d\n", value)
}

输出结果为:

Value: 10
Value: 20
Value: 30
Value: 40

关键字 range 总是会从切片头部开始迭代。如果想对迭代做更多的控制,则可以使用传统的 for 循环,代码如下所示。:

使用传统的 for 循环对切片进行迭代
// 创建一个整型切片,并赋值
slice := []int{
    
    10, 20, 30, 40}
// 从第三个元素开始迭代每个元素
for index := 2; index < len(slice); index++ {
    
    
    fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

输出结果为:

Index: 2 Value: 30
Index: 3 Value: 40

在前面的学习中我们了解了两个特殊的内置函数 len() 和 cap(),可以用于处理数组、切片和通道,对于切片,函数 len() 可以返回切片的长度,函数 cap() 可以返回切片的容量,在上面的示例中,使用到了函数 len() 来控制循环迭代的次数。

当然,range 关键字不仅仅可以用来遍历切片,它还可以用来遍历数组、字符串、map 或者通道等。

映射Map

映射Map是一个存储键值对的无序集合,映射Map是一种巧妙并且实用的数据结构。

它是一个无序的key/value对的集合,其中所有的key都是不同的,然后通过给定的key可以在常数时间复杂度内检索、更新或删除对应的value。

在Go语言中,一个map就是一个映射Map的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。

map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。

其中K对应的key必须是支持==比较运算符的数据类型,所以map可以通过测试key是否相等来判断是否已经存在。

map的申明和创建

  1. Map的声明
var m map[string] string

m是声明的变量名,sting是对应的Key的类型,string是value的类型。

  1. 创建

Go内置的make函数可以创建map:

 m := make(map[string]int)

我们也可以用map字面值的语法创建map,同时还可以指定一些最初的key/value:

m := map[string]int{
    
    
"keke": 001,
"jame": 002,
}

这个等价于:

m := make(map[string]int)
m["keke"] = 001
m["jame"] = 002

另外一种创建map的方式是map[string]int{}。

3.元素的删除

Map可以使用内置的delete函数可以删除元素:

delete(ages, "jame") //可以删除m["jame"]

map的使用案例

在 Go 中,map 是一种无序的键值对数据结构。它可以用于存储和访问任意类型的数据,并提供了各种强大的操作和函数。

以下是一个基本的 map 定义和使用方式:



func MapDemo() {
    
    
    // 声明并初始化一个空的 map
    var m map[string]int = make(map[string]int)

    // 设置 map 中的键值对
    m["apple"] = 10
    m["banana"] = 20
    m["orange"] = 30

    // 打印 map 中的值
    fmt.Println(m)

    // 获取 map 中的一个值
    fmt.Println(m["banana"])

    // 删除 map 中的一个键值对
    delete(m, "orange")
    fmt.Println(m)

    // 检查 map 中是否包含某个键
    if v, ok := m["apple"]; ok {
    
    
        fmt.Printf("m[\"apple\"] = %d\n", v)
    }

    // 遍历 map 并打印每个键值对
    for k, v := range m {
    
    
        fmt.Printf("%s: %d\n", k, v)
    }
}

在该示例中,我们首先使用 make 函数声明并初始化了一个空的 map m

我们通过 m["key"] = value 的方式向 map 中添加了三个键值对,并通过 fmt.Println 函数打印了这个 map 的值。

接下来,我们演示了如何获取、删除和检查 map 中的键值对,并通过 for range 循环遍历了整个 map 并打印了每个键值对的值。

需要注意的是,在使用 map 时,我们需要特别注意其键类型和值类型,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。同时,我们也要了解 map 的底层实现方式,以便在处理大型数据集和高性能应用程序时保持最佳性能。

禁止对map元素取址

map中的元素并不是一个变量,因此我们不能对map的元素进行取址操作:

_ = &ages["keke"] // compile error: cannot take address of map element

禁止对map元素取址的原因是:map可能随着元素数量的增长而重新分配更大的内存空间,从而可能导致之前的地址无效。

要想遍历map中全部的key/value对的话,可以使用range风格的for循环实现,和之前的slice遍历语法类似。下面的迭代语句将在每次迭代时设置name和age变量,它们对应下一个键/值对:

for k, v := range m {
    
    
    fmt.Printf("%s\t%d\n", k, v)
}

Map的迭代顺序是不确定的,并且不同的哈希函数实现可能导致不同的遍历顺序。

在实践中,遍历的顺序是随机的,每一次遍历的顺序都不相同。这是故意的,每次都使用随机的遍历顺序可以强制要求程序不会依赖具体的哈希函数实现。

如果要按顺序遍历key/value对,我们必须显式地对key进行排序,可以使用sort包的Strings函数对字符串slice进行排序。常见的处理方式:

import "sort"
var names []string
for name := range ages {
    
    
    names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
    
    
    fmt.Printf("%s\t%d\n", name, ages[name])
}

map类型的零值是nil,也就是没有引用任何映射Map。

var ages map[string]int
fmt.Println(ages == nil)  // "true"
fmt.Println(len(ages) == 0) // "true"

结构体

Go提供的结构体就是把使用各种数据类型定义的不同变量组合起来的高级数据类型。

type Rectangle struct {
    
    
    width float64
    length float64
}

通过type定义一个新的数据类型,然后是新的数据类型名称rectangle,最后是struct关键字,表示这个高级数据类型是结构体类型。

一个结构体的例子

在 Go 中,结构体是一种自定义的数据类型,用于组合不同类型的字段以表示复杂的实体或概念。

结构体可以包含任何类型的字段,包括基本类型、数组、切片、map、函数等。

以下是一个基本的结构体定义和使用方式:

// 定义一个结构体类型
type person struct {
    
    
    name string
    age  int
}

func StructDemo() {
    
    
    // 使用结构体类型创建两个变量
    p1 := person{
    
    "Alice", 18}
    p2 := person{
    
    name: "Bob", age: 20}

    // 打印结构体变量的值
    fmt.Println(p1)
    fmt.Println(p2)

    // 修改结构体中的字段
    p1.age = 19
    p2.name = "Charlie"
    fmt.Println(p1)
    fmt.Println(p2)
}

输出如下:

{
    
    Alice 18}
{
    
    Bob 20}
{
    
    Alice 19}
{
    
    Charlie 20}

在该示例中,我们首先通过 type 关键字定义了一个名为 person 的结构体类型,并声明了两个字段 nameage。接着,我们使用该结构体类型创建了两个变量 p1p2,并通过花括号进行了初始化。

我们通过 fmt.Println 函数打印了这两个结构体变量的值,并分别修改了其中的一个字段,并再次打印了它们的值。

需要注意的是,在使用结构体时,我们需要特别注意字段名称和类型,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。

同时,我们也要掌握结构体与方法、嵌套结构体、指针等高级特性,以便在处理复杂的数据结构和面向对象编程时保持最佳性能和代码质量。

其实构体类型和基础数据类型使用方式差不多,唯一的区别就是结构体类型可以通过.来访问内部的成员。包括给内部成员赋值和读取内部成员值。

如果你知道结构体成员定义的顺序,也可以不使用key:value的方式赋值,直接按照结构体成员定义的顺序给它们赋值。


type Rectangle struct {
    
    
    width float64
   length float64
}
func main() {
    
    
   var r = Rectangle{
    
    100, 200}
   fmt.Println("Width:", r.width, "* Length:",r.length, "= Area:", r.width*r.length)
}

输出结果为:

 Width: 100 * Length: 200 = Area: 20000

结构体参数传递方式

Go函数的参数传递方式是值传递,这句话对结构体也是适用的。

package main
import (
    "fmt"
)
type Rectangle struct {
    
    
    width float64
   length float64
}
func double_area(r Rectangle) float64 {
    
    
    r.width *= 2
    r.length *= 2
    return r.width * r.length
}
func main() {
    
    
    var r = Rectangle{
    
    100, 200}
    fmt.Println(double_area(r))
    fmt.Println("Width:", r.width, "Length:", r.length)
}

输出为:

80000
Width: 100 Length: 200

虽然在double_area函数里面我们将结构体的宽度和长度都加倍,但仍然没有影响main函数里面的rect变量的宽度和长度。

要想改变函数输出的长方形的值,我们使用指针可以做到。

指针的主要作用就是在函数内部改变传递进来变量的值。

package main
import (
    "fmt"
)
type Rectangle struct {
    
    
    width  float64
    length float64
}
func (r *Rectangle) area() float64 {
    
    
    return r.width * r.length
}
func main() {
    
    
  var r = new(Rectangle)
  r.width = 100
  r.length = 200
  fmt.Println("Width:", r.width, "Length:", r.length,"Area:", r.area())
}

使用了new函数来创建一个结构体指针Rectangle,也就是说r的类型是Rectangle,结构体遇到指针的时候,你不需要使用去访问结构体的成员,直接使用.引用就可以了。

所以上面的例子中我们直接使用r.width=100 和r.length=200来设置结构体成员值。

因为这个时候r是结构体指针,所以我们定义area()函数的时候结构体限定类型为*Rectangle。

结构体组合函数

上面我们在main函数中计算了矩形的面积,但是我们觉得矩形的面积如果能够作为矩形结构体的“内部函数”提供会更好。

这样我们就可以直接说这个矩形面积是多少,而不用另外去取宽度和长度去计算。现在我们看看结构体“内部函数”定义方法:

package main
import (
    "fmt"
)
type Rect struct {
    
    
    width, length float64
}
func (rect Rect) area() float64 {
    
    
    return rect.width * rect.length
}
func main() {
    
    
    var rect = Rect{
    
    100, 200}
    fmt.Println("Width:", rect.width, "Length:", rect.length,
        "Area:", rect.area())
}

咦?这个是什么“内部方法”,根本没有定义在Rect数据类型的内部啊?

确实如此,我们看到,虽然main函数中的rect变量可以直接调用函数area()来获取矩形面积,但是area()函数确实没有定义在Rect结构体内部,这点和C语言的有很大不同。

Go使用组合函数的方式,来为结构体定义结构体方法。

我们仔细看一下上面的area()函数定义。

首先是关键字func表示这是一个函数,第二个参数是结构体类型和实例变量,第三个是函数名称,第四个是函数返回值

这里我们可以看出area()函数和普通函数定义的区别就在于area()函数多了一个结构体类型限定。这样一来Go就知道了这是一个为结构体定义的方法。

这里需要注意一点就是定义在结构体上面的函数(function)一般叫做方法(method)

这是为何呢?

我们看到,虽然main函数中的rect变量可以直接调用函数area()来获取矩形面积,但是area()函数确实没有定义在Rect结构体内部,这点和C语言的有很大不同。

结构体特性

如果结构体成员名字是以大写字母开头的,那么该成员就是导出的;这是Go语言导出规则决定的。

一个结构体可能同时包含导出和未导出的成员. 直白的讲就是首字母大写的结构体字段可以被导出,也就是说,在其他包中可以进行读写。结构体字段名以小写字母开头是当前包的私有的,函数定义也是类似的。

如果结构体没有任何成员的话就是空结构体,写作struct{}。它的大小为0,也不包含任何信息,但是有时候依然是有价值的。

有些Go开发者用map模拟set数据结构时,用它来代替map中布尔类型的value,只是强调key的重要性,但是因为节约的空间有限,而且语法比较复杂,所有我们通常避免避免这样的用法。

一个命名为S的结构体类型将不能再包含S类型的成员:因为一个聚合的值不能包含它自身(该限制同样适应于数组)。但是S类型的结构体可以包含*S指针类型的成员,这可以让我们创建递归的数据结构,比如链表和树结构等。

type tree struct {
    
    
    value  int
    left, right *tree
}

结构体嵌入和匿名成员

type Point struct {
    
    
    X, Y int
}
type Circle struct {
    
    
    Center Point
    Radius int
}
type Wheel struct {
    
    
    Circle Circle
    Spokes int
}

访问每个成员:

var w Wheel
w.Circle.Center.X = 8
w.Circle.Center.Y = 8
w.Circle.Radius = 5
w.Spokes = 20

Go语言有一个特性让我们只声明一个成员对应的数据类型,而不指名成员的名字,这类成员就叫匿名成员

匿名成员的数据类型必须是命名的类型或指向一个命名的类型的指针。

下面的代码中,Circle和Wheel各自都有一个匿名成员。我们可以说Point类型被嵌入到了Circle结构体,同时Circle类型被嵌入到了Wheel结构体。

type Circle struct {
    
    
    Point  // 匿名字段,struct
    Radius int
}
type Wheel struct {
    
    
    Circle  // 匿名字段,struct
    Spokes int
}

得意于匿名嵌入的特性,我们可以直接访问叶子属性而不需要给出完整的路径:

var w Wheel
w.X = 8            // equivalent to w.Circle.Point.X = 8
w.Y = 8            // equivalent to w.Circle.Point.Y = 8
w.Radius = 5       // equivalent to w.Circle.Radius = 5
w.Spokes = 20

在右边的注释中给出的显式形式访问这些叶子成员的语法依然有效,因此匿名成员并不是真的无法访问了。其中匿名成员Circle和Point都有自己的名字——就是命名的类型名字——但是这些名字在点操作符中是可选的。

我们在访问子成员的时候可以忽略任何匿名成员部分。

结构体字面值并没有简短表示匿名成员的语法,因此下面的语句都不能编译通过:

w = Wheel{
    
    8, 8, 5, 20}   // compile error: unknown fields
w = Wheel{
    
    X: 8, Y: 8, Radius: 5, Spokes: 20} // compile error: unknown fields

结构体字面值必须遵循形状类型声明时的结构,所以我们只能用下面的两种语法,它们彼此是等价的.

通过这个我们看出来struct不仅仅能够将struct作为匿名字段,自定义类型、内置类型都可以作为匿名字段,而且可以在相应的字段上面进行函数操作。

结构体tag

在Golang中结构体和数据库表的映射关系的建立是通过struct Tag来实现的。

package main
import (
      "fmt"
      "reflect" // 这里引入reflect模块
)
type User struct {
    
    
     Name    string `json:"name"`  //这引号里面的就是tag
     Passwd  int    `json:"passwd"`
}
func main() {
    
    
     user := &User{
    
    "keke", 123456}
     s := reflect.TypeOf(user).Elem() //通过反射获取type定义
     for i := 0; i < s.NumField(); i++ {
    
    
         fmt.Println(s.Field(i).Tag.Get("json")) //将tag输出出来
     }
 }

运行 :

name
passwd

结构体的成员Tag可以是任意的字符串面值,但是通常是一系列用空格分隔的key:”value”键值对序列;因为值中含义双引号字符,因此成员Tag一般用原生字符串面值的形式书写。

这里有必要解释下指针这块,在Golang中很多时候都是需要用指针结合结构体开发的。

通常指针是存储一个变量的内存地址的变量。

在Golang中,指针不参与计算但是可以用来获取地址的,例如变量a的内存地址为&a,这里的&就是获取a的地址。如果一个指针,它的值是在别的地方的地址,而且我们想要获取这个地址的值,可以使用*符号*符号是为取值符。例如上面的&a是一个地址,那么这个地址里存储的值为*&a

& 和 *符号的区别

注意:这里的 & 和 *符号的区别,& 运算符,用来获取指针地址,而*运算符是用来获取地址里存储的值。

此外指针的值和指针的地址是不同的概念,指针的值: 指的是一个地址,是别的内存地址。指针的地址: 指的是存储指针内存块的地址。

通常 & 运算符是对变量取地址,如:变量a的地址是&a, *符号运算符对指针取值,如:*&a,就是a变量所在地址的值,也就是a的值.此外 &和 * 以互相抵消,同时注意,*&可以抵消掉,但&*是不可以抵消的, a和 *&a是一样的,都是a的值,因为* & 互相抵消掉了,同理a和*&*&*&*&a是一样的 (因为4个*&互相抵消掉了)。

var a = 2
var b *int = &a
所以a和*&a和*b是一样的,都是a的值,值为2 (把b当做&a看)

应用示例:

package main
import (
    "fmt"
)
func main(){
    
    
   b := 200
   a := &b
   fmt.Println("the address of b:",a)
   fmt.Println("the value of b:",*a)
   var p *int  //p的类型是[int型的指针]
   p = &b     //p的值为 [b的地址]
   fmt.Printf("b=%d,p=%d,*p=%d \n",b,p,*p)
   *p = 5 // *p的值为[[b的地址]的指针] (其实就是b),这行代码也就等价于b= 5
    fmt.Printf("b=%d,p=%d,*p=%d\n",b,p,*p)
}

运行:

the address of b: 0xc4200180b8
the value of b: 200
b=200,p=842350559416,*p=200 
b=5,p=842350559416,*p=5

通常我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

传指针比较轻量级 (*bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话,在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。

Golang中string,slice,map这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。

注意:若函数需改变slice的长度,则仍需要取地址传递指针。

如果要访问指针 p 指向的结构体中某个元素 x,不需要显式地使用 * 运算,可以直接 p.x;

JSON

JavaScript对象表示法(JSON)是一种用于发送和接收结构化信息的标准协议。

在类似的协议中,JSON并不是唯一的一个标准协议。

XML、ASN.1和Google的Protocol Buffers都是类似的协议,并且有各自的特色,但是由于简洁性、可读性和流行程度等原因,JSON是应用最广泛的一个。

JSON是对JavaScript中各种类型的值——字符串、数字、布尔值和对象——Unicode本文编码。

基本的JSON类型有数字(十进制或科学记数法)、布尔值(true或false)、字符串,

其中字符串是以双引号包含的Unicode字符序列,支持和Go语言类似的反斜杠转义特性,

不过JSON使用的是\Uhhhh转义数字来表示一个UTF-16编码(译注:UTF-16和UTF-8一样是一种变长的编码,有些Unicode码点较大的字符需要用4个字节表示;而且UTF-16还有大端和小端的问题),而不是Go语言的rune类型。

这些基础类型可以通过JSON的数组和对象类型进行递归组合。

一个JSON数组是一个有序的值序列,写在一个方括号中并以逗号分隔;一个JSON数组可以用于编码Go语言的数组和slice。

一个JSON对象是一个字符串到值的映射,写成以系列的name:value对形式,用花括号包含并以逗号分隔;JSON的对象类型可以用于编码Go语言的map类型(key类型是字符串)和结构体。

解析JSON

数据结构 --> 指定格式 = 序列化 或 编码(传输之前)
指定格式 --> 数据格式 = 反序列化 或 解码(传输之后)

序列化是在内存中把数据转换成指定格式(data -> string),反之亦然(string -> data structure)。

编码也是一样的,只是输出一个数据流(实现了 io.Writer 接口);解码是从一个数据流(实现了 io.Reader)输出到一个数据结构。

json字符串:

{
    
    "servers":[{
    
    "serverName":"Shanghai","serverIP":"127.0.0.1"},{
    
    "serverName":"Beijing","serverIP":"127.0.0.2"}]}

反序列化:json解析到结构体

在Go中我们经常需要做数据结构的转换,Go中提供的处理json的标准包是 encoding/json,主要使用的是以下两个方法:

// 序列化 结构体=> json  
func Marshal(v interface{
    
    }) ([]byte, error)
// 反序列化 json=>结构体
func Unmarshal(data []byte, v interface{
    
    }) error

序列化前后的数据结构有以下的对应关系:

bool    for JSON  booleans
float64 for JSON  numbers
string  for JSON strings
[]interface{
    
    }  for JSON arrays
map[string]interface{
    
    }  for JSON objects
nil     for JSON null

如果我们有一段json要转换成结构体就需要用到(Unmarshal)样的函数:

func Unmarshal(data []byte, v interface{
    
    }) error

通过这个函数我们就可以实现解析:

package main
import (
    "encoding/json"
    "fmt"
)
type Server struct {
    
    
      ServerName string
      ServerIP   string
}
type Serverslice struct {
    
    
      Servers []Server
}
func main() {
    
    
     var s Serverslice
     str := `{"servers":[{"serverName":"Shanghai","serverIP":"127.0.0.1"},    {"serverName":"Beijing","serverIP":"127.0.0.2"}]}`
     json.Unmarshal([]byte(str), &s)
     fmt.Println(s)
}

我们首先定义了与json数据对应的结构体,数组对应slice,字段名对应JSON里面的KEY,在解析的时候,如何将json数据与struct字段相匹配呢?例如JSON的key是Foo,那么怎么找对应的字段呢?这就用到了我们上面结构体说的tag。

同时能够被赋值的字段必须是可导出字段(即首字母大写)。同时JSON解析的时候只会解析能找得到的字段,找不到的字段会被忽略,这样的一个好处是:当你接收到一个很大的JSON数据结构而你却只想获取其中的部分数据的时候,你只需将你想要的数据对应的字段名大写,即可轻松解决这个问题。

序列化:结构体转json

结构体转json就需要用到JSON包里面通过Marshal函数来处理:

func Marshal(v interface{
    
    }) ([]byte, error)

假设我们还是需要生成上面的服务器列表信息,那么如何来处理呢?

package main
import (
    "encoding/json"
    "fmt"
)
type Server struct {
    
    
    ServerName string
    ServerIP   string
}
type Serverslice struct {
    
    
    Servers []Server
}
func main() {
    
    
    var s Serverslice
    s.Servers = append(s.Servers, Server{
    
    ServerName: "Shanghai", ServerIP: "127.0.0.1"})
    s.Servers = append(s.Servers, Server{
    
    ServerName: "Beijing", ServerIP: "127.0.0.2"})
    b, err := json.Marshal(s)
    if err != nil {
    
    
        fmt.Println("json err:", err)
    }
    fmt.Println(string(b))
}

输出结果:

{
    
    "Servers":[{
    
    "ServerName":"Shanghai","ServerIP":"127.0.0.1"},{
    
    "ServerName":"Beijing","ServerIP":"127.0.0.2"}]}

我们看到上面的输出字段名的首字母都是大写的,如果你想用小写的首字母怎么办呢?

把结构体的字段名改成首字母小写的?

JSON输出的时候必须注意,只有导出的字段才会被输出,如果修改字段名,那么就会发现什么都不会输出,所以必须通过struct tag定义来实现:

type Server struct {
    
    
    ServerName string `json:"serverName"`
    ServerIP   string `json:"serverIP"`
}
type Serverslice struct {
    
    
    Servers []Server `json:"servers"`
}

针对JSON的输出,我们在定义struct tag的时候需要注意的几点是:

  • 字段的tag是”-“,那么这个字段不会输出到JSON
  • tag中带有自定义名称,那么这个自定义名称会出现在JSON的字段名中,例如上面例子中serverName
  • tag中如果带有”omitempty”选项,那么如果该字段值为空,就不会输出到JSON串中
  • 如果字段类型是bool, string, int, int64等,而tag中带有”,string”选项,那么这个字段在输出到JSON的时候会把该字段对应的值转换成JSON字符串
type Server struct {
    
    
    // ID 不会导出到JSON中
    ID int `json:"-"`
    // ServerName2 的值会进行二次JSON编码
    ServerName  string `json:"serverName"`
    ServerName2 string `json:"serverName2,string"`
    // 如果 ServerIP 为空,则不输出到JSON串中
    ServerIP   string `json:"serverIP,omitempty"`
}
s := Server {
    
    
    ID:         1,
    ServerName:  `Go "1.0" `,
    ServerName2: `Go "1.10" `,
    ServerIP:   ``,
}
b, _ := json.Marshal(s)
os.Stdout.Write(b)

输出内容:

{
    
    "serverName":"Go \"1.0\" ","serverName2":"\"Go \\\"1.10\\\" \""}

Marshal函数只有在转换成功的时候才会返回数据,但是我们应该注意下:

  • JSON对象只支持string作为key,所以要编码一个map,那么必须是map[string]T这种类型(T是Go语言中任意的类型)
  • Channel, complex和function是不能被编码成JSON的
  • 嵌套的数据是不能编码的,不然会让JSON编码进入死循环
  • 指针在编码的时候会输出指针指向的内容,而空指针会输出null
  • 解析到interface

在Go中Interface{}可以用来存储任意数据类型的对象,这种数据结构正好用于存储解析的未知结构的json数据的结果。JSON包中采用map[string]interface{}和[]interface{}结构来存储任意的JSON对象和数组。Go类型和JSON类型的对应关系如下:

  • bool 代表 JSON booleans,
  • loat64 代表 JSON numbers,
  • string 代表 JSON strings,
  • nil 代表 JSON null.

通常情况下我们会拿到一段json数据:

b := []byte(`{
    
    "Name":"Ande","Age":10,"Hobby":"Football"

现在开始解析到接口中:

var f interface{
    
    }
err := json.Unmarshal(b, &f)

在这个接口f里面存储了一个map类型,他们的key是string,值存储在空的interface{}里

f = map[string]interface{
    
    }{
    
    
    "Name": "Ande",
    "Age":  ,
    "Hobby":"Football"
}

通过断言的方式我们把结构体强制转换数据类型:

m := f.(map[string]interface{
    
    })

通过断言之后,我们就可以通过来访问里面的数据:

for k, v := range m {
    
    
    switch vv := v.(type) {
    
    
    case string:
        fmt.Println(k, "is string", vv)
    case int:
        fmt.Println(k, "is int", vv)
    case float64:
        fmt.Println(k,"is float64",vv)
    case []interface{
    
    }:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
    
    
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

通过interface{}与type assert的配合,我们就可以解析未知结构的JSON数了。

其实很多时候我们通过类型断言,操作起来不是很方便,我们可以使用一个叫做simplejson的包,在处理未知结构体的JSON数据:

data,err := NewJson([]byte(`{                
    "test": {
        "array": [1, "2", 3, 4, 5]
        "int": 10,
        "float": 5.150,
        "bignum": 9223372036854775807,
        "string": "simplejson",
        "bool": true
    }
}`))
arr, _ := js.Get("test").Get("array").Array()
i, _ := js.Get("test").Get("int").Int()
ms := js.Get("test").Get("string").MustString()

这个库用起来还是很方便也很简单。

json中的Encoder和Decoder

通常情况下,我们可能会从 Request 之类的输入流中直接读取 json 进行解析或将编码(encode)的 json 直接输出,为了方便,标准库为我们提供了 Decoder 和 Encoder 类型。它们分别通过一个 io.Reader 和 io.Writer 实例化,并从中读取数据或写数据。

#Decoder 从 r io.Reader 中读取数据,`Decode(v interface{})` 方法把数据转换成对应的数据结构
func NewDecoder(r io.Reader) *Decoder
#Encoder 的 `Encode(v interface{})` 把数据结构转换成对应的 JSON 数据,然后写入到 w io.Writer 中
func NewEncoder(w io.Writer) *Encoder

查看源码可以发现,Encoder.Encode/Decoder.DecodeMarshal/Unmarshal 实现大体是一样。 但是也有一些不同点:Decoder 有一个方法 UseNumber,它的作用是,在默认情况下,json 的 number 会映射为 Go 中的 float64,有时候,这会有些问题,比如:

b := []byte(`{"Name":"keke","Age":25,"Money":200.3}`)
var person = make(map[string]interface{
    
    })
err := json.Unmarshal(b, &person)
if err != nil {
    
    
   log.Fatalln("json unmarshal error:", err)
}
age := person["Age"]
log.Println(age.(int))

运行:

>  interface conversion: interface is float64, not int. #age是int,结果 panic 了

然后我们改用Decoder.Decode(用上 UseNumber):

b := []byte(`{"Name":"keke","Age":25,"Money":200.3}`)
var person = make(map[string]interface{
    
    })
decoder := json.NewDecoder(bytes.NewReader(b))
decoder.UseNumber()
err := decoder.Decode(&person)
if err != nil {
    
    
    log.Fatalln("json unmarshal error:", err)
}
age := person["Age"]
log.Println(age.(json.Number).Int64())
  • json.RawMessage能够延迟对 json进行解码

此外我们在解析的时候,还可以把某部分先保留为 JSON 数据不要解析,等到后面得到更多信息的时候再去解析。

流程控制语句:for、if、else、switch 和 defer

for语句

Go 只有一种循环结构:for 循环。

基本的 for 循环由三部分组成,它们用分号隔开:

  • 初始化语句:在第一次迭代前执行
  • 条件表达式:在每次迭代前求值
  • 后置语句:在每次迭代的结尾执行
    初始化语句通常为一句短变量声明,该变量声明仅在 for 语句的作用域中可见。
    一旦条件表达式的布尔值为 false,循环迭代就会终止。

注意

和 C、Java、JavaScript 之类的语言不同,Go 的 for 语句后面的三个构成部分外没有小括号, 大括号 { } 则是必须的。

for 使用案例

在 Go 中,for 语句用于循环执行代码块多次,直到满足某个条件为止。Go 提供了三种形式的 for 循环:基本循环、条件循环和 range 循环。

以下是一个基本的 for 循环示例:

package main

import "fmt"

func main() {
    
    
    // 基本循环
    for i := 0; i < 5; i++ {
    
    
        fmt.Println(i)
    }

    // 条件循环
    j := 0
    for j < 5 {
    
    
        fmt.Println(j)
        j++
    }

    // range 循环
    s := []string{
    
    "foo", "bar", "baz"}
    for i, v := range s {
    
    
        fmt.Printf("s[%d]: %s\n", i, v)
    }
}

在该示例中,我们首先使用基本循环形式 for init; condition; post { } 循环输出 0 ~ 4 的整数。接着,我们使用条件循环形式 for condition { } 实现同样的效果,并输出相同的结果。

最后,我们使用 range 循环形式 for index, value := range array { } 遍历了切片 s 并打印了其中每个元素的下标和值。

需要注意的是,在使用 for 循环时,我们需要特别注意循环条件和计数器的初始化和更新方式,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。同时,我们也要了解 for 循环与 breakcontinuegoto 等语句的配合使用,以便实现更复杂的控制流程和算法逻辑。

for 是 Go 中的 “while”

此时你可以去掉分号,因为 C 的 while 在 Go 中叫做 for

package main
import "fmt"
func main() {
    
    
    sum := 1
    for sum < 1000 {
    
    
        sum += sum
    }
    fmt.Println(sum)
}

无限循环

如果省略循环条件,该循环就不会结束,因此无限循环可以写得很紧凑。

package main
func main() {
    
    
    for {
    
    
    }
}

if语句

Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。

在 Go 中,if 语句用于根据条件执行不同的代码块。Go 还提供了 elseelse if 和嵌套 if 等语法来实现更复杂的逻辑判断和控制流程。

以下是一个基本的 if 语句示例:

package main

import "fmt"

func main() {
    
    
    // 基本 if 语句
    x := 10
    if x > 5 {
    
    
        fmt.Println("x is greater than 5")
    }

    // if else 语句
    y := 3
    if y > 5 {
    
    
        fmt.Println("y is greater than 5")
    } else {
    
    
        fmt.Println("y is less than or equal to 5")
    }

    // if else if 语句
    z := 0
    if z > 0 {
    
    
        fmt.Println("z is positive")
    } else if z < 0 {
    
    
        fmt.Println("z is negative")
    } else {
    
    
        fmt.Println("z is zero")
    }
}

在该示例中,我们首先使用基本 if 语句形式 if condition { } 判断变量 x 是否大于 5,并输出相应的结果。接着,我们使用 if else 形式 if condition { } else { } 判断变量 y 是否大于 5,并输出相应的结果。

最后,我们使用 if else if 形式 if condition1 { } else if condition2 { } else { } 对变量 z 进行了三重判断,并输出相应的结果。

需要注意的是,在使用 if 语句时,我们需要特别注意条件表达式和花括号的位置,并尽可能地遵循最佳实践,以确保代码的正确性和可读性。同时,我们也要了解 if 语句与布尔运算符、短变量声明、类型断言等语法的配合使用,以便在处理更复杂的逻辑和算法时保持最佳性能和代码质量。

if 后的简短语句

for 一样, if 语句可以在if后面,条件表达式前执行一个简单的语句。该语句声明的变量作用域仅在 if 之内。

func PowIf(x, n, lim float64) float64 {
    
    
    if v := math.Pow(x, n); v < lim {
    
    
        return v
    }
    return lim
}

在最后的 return 语句处使用 v 看看。 是不可见的。

switch 语句

switch 是编写一连串 if - else 语句的简便方法。

switch 匹配逻辑是:匹配第一个值等于条件表达式的 case 语句。

Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。

实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。

Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。

switch 的例子

func SwitchDemo() {
    
    
    fmt.Print("Go runs on ")
    switch os := runtime.GOOS; os {
    
    
    case "darwin":
        fmt.Println("OS X.")
    case "linux":
        fmt.Println("Linux.")
    default:
        // freebsd, openbsd,
        // plan9, windows...
        fmt.Printf("%s.\n", os)
    }
}

switch 的求值顺序

switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。

例如,

switch i {
    
    
    case 0:
    case f():
 }

i==0f 不会被调用。

没有条件的 switch

没有条件的 switch 同 switch true 一样。这种形式能将一长串 if-then-else 写得更加清晰。

func main() {
    
    
    t := time.Now()
    switch {
    
    
    case t.Hour() < 12:
        fmt.Println("Good morning!")
    case t.Hour() < 17:
        fmt.Println("Good afternoon.")
    default:
        fmt.Println("Good evening.")
    }
}

defer语句

defer 语句会将函数推迟到外层函数返回之后执行。

推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。

func DeferDemo() {
    
    
    defer fmt.Println("world")
    fmt.Println("hello")
}

执行结果:

hello
world

defer 栈

推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

func DeferDemo2() {
    
    
	fmt.Println("counting")
	for i := 0; i < 10; i++ {
    
    
		defer fmt.Println(i)
	}
	fmt.Println("done")
}

执行结果

counting
done
9
8
7
6
5
4
3
2
1

panic异常

在 Go 中,panic 是一种用于在程序出现严重错误时引发异常的机制。当程序遇到无法处理的错误或不可恢复的情况时,可以使用 panic 函数来立即停止程序并输出相应的异常信息。

panic 使用示例

以下是一个简单的 panic 示例:

package main

import "fmt"

func main() {
    
    
    // 引发 panic 异常
    panic("a problem occurred")

    // 下面的代码将不会被执行
    fmt.Println("Hello, World!")
}

在该示例中,我们使用 panic 函数引发了一个名为 “a problem occurred” 的异常,并停止了程序的执行。由于 panic 函数立即停止程序,因此下面的 fmt.Println 语句将不会被执行。

需要注意的是,在使用 panic 机制时,我们需要特别注意异常类型和异常信息的准确性和清晰度,并尽可能地遵循最佳实践,以确保代码的可读性和可维护性。同时,我们也要了解 recover 函数的使用方法和场景,以便在必要时恢复程序的执行并处理异常情况。

错误和异常

错误和异常这两个是不同的概念,非常容易混淆。

很多人习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误。

错误指的是可能出现问题的地方出现了问题,比如压缩一个文件时失败,这种情况在人们可以意料之中的事情;但是异常指的是不应该出现问题的地方出现了问题,比如引用了空指针,这种情况在人们的意料之外。

因而,错误是业务过程的一部分,而异常不是。

在 Go 中,error 是一种用于描述程序执行过程中可能出现的错误状态的类型。当程序遇到某个异常情况时,可以通过返回一个 error 类型值来通知调用者并进行相应的处理。

Golang中引入error接口类型作为错误处理的标准模式,如果函数要返回错误,则返回值类型列表中肯定包含error。

error处理过程类似于C语言中的错误码,可逐层返回,直到被处理。

以下是一个简单的 error 示例:

package main

import (
    "fmt"
    "errors"
)

func divide(x, y float64) (float64, error) {
    
    
    if y == 0 {
    
    
        return 0, errors.New("division by zero")
    }
    return x / y, nil
}

func main() {
    
    
    // 调用函数并检查是否有错误发生
    result, err := divide(10.0, 2.0)
    if err != nil {
    
    
        fmt.Println(err)
        return
    }
    fmt.Println(result)

    // 再次调用函数并检查是否有错误发生
    result, err = divide(5.0, 0.0)
    if err != nil {
    
    
        fmt.Println(err)
        return
    }
    fmt.Println(result)
}

在该示例中,我们定义了一个名为 divide 的函数,用于计算两个浮点数的商,并返回相应的结果和错误信息。当除数为零时,我们使用 errors.New 函数创建了一个新的 error 类型值并将其返回。在 main 函数中,我们分别调用 divide 函数并检查是否有错误发生,如果有则打印相应的错误信息。

需要注意的是,在使用 error 机制时,我们需要特别注意错误类型和错误信息的准确性和清晰度,并尽可能地遵循最佳实践,以确保代码的可读性和可维护性。同时,我们也要了解 defer 函数的使用方法和场景,以便在必要时释放资源并处理异常情况。

异常处理流程

Golang中引入两个内置函数panic和recover来触发和终止异常处理流程,同时引入关键字defer来延迟执行defer后面的函数。

一直等到包含defer语句的函数执行完毕时,延迟函数(defer后的函数)才会被执行,而不管包含defer语句的函数是通过return的正常结束,还是由于panic导致的异常结束。你可以在一个函数中执行多条defer语句,它们的执行顺序与声明顺序相反。

当程序运行时候,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则会先触发panic函数的执行,然后调用延迟函数。

调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。

如果一路在defer延迟函数中没有recover函数的调用,则会到达协程的起点,该协程结束,然后终止其他所有协程,包括主协程.

错误和异常从Golang机制上讲,就是error和panic的区别。

很多其他语言也一样,比如C++/Java,没有error但有errno,没有panic但有throw。

一般而言,当panic异常发生时,程序会中断运行,并立即执行在该goroutine中被延迟的函数(defer 机制)。随后,程序崩溃并输出日志信 息。

日志信息包括panic value和函数调用的堆栈跟踪信息。panic value通常是某种错误信息。对于每个goroutine,日志信息中都会有与之相对的,发生panic时的函数调用堆栈跟踪信息。

通常,我们不需要再次运行程序去定位问题,日志信息已经提供了足够的诊断依据。因此,在我们填写问题报告时,一般会将panic异常和日志信息一并记录。

func panic(interface{
    
    })

虽然Go的panic机制,非常类似于其他语言的异常,但panic的适用场景有一些不同。

由于panic会引起程序的崩溃,因此panic一般用于严重错误,如程序内部的逻辑不一致。

通常认为任何崩溃都表明代码中存在漏洞,所以对于大部分漏洞,我们应该使用Go提供的错误机制,而不是panic,尽量避免程序的崩溃。

在健壮的程序中,任何可以预料到的错误,如不正确的输入、错误的配置或是失败的I/O操作都应该被优雅的处理,最好的处理方式,就是使用Go的错误机制。

recover捕获异常

通常来说,不应该对panic异常做任何处理,但有时,也许我们可以从异常中恢复,至少我们可以在程序崩溃前,做一些操作。

如果在deferred函数中调用了内置函数recover,并且定义该defer语句的函数发生了panic异常,recover会使程序从panic中恢复,并返回panic value。导致panic异常的函数不会继续运行,但能正常返回。

在未发生panic时调用recover,recover会返回nil。

func recover() interface{
    
    }

recover 函数用来获取 panic 函数的参数信息,只能在延时调用 defer 语句调用的函数中直接调用才能生效,如果在 defer 语句中也调用 panic 函数,则只有最后一个被调用的 panic 函数的参数会被 recover 函数获取到。

如果 goroutine 没有 panic,那调用 recover 函数会返回 nil。

以语言解析器为例,说明recover的使用场景。

考虑到语言解析器的复杂性,即使某个语言解析器目前工作正常,也无法肯定它没有漏洞。

因此,当某个异常出现时,我们不会选择让解析器崩溃,而是会将panic异常当作普通的解析错误,并附加额外信息提醒用户报告此错误。

func Parse(input string) (s *Syntax, err error) {
    
    
  defer func() {
    
    
    if p := recover(); p != nil {
    
    
      err = fmt.Errorf("internal error: %v", p)
    }
 }()
 // ...parser...
}

从中可以看到defer函数帮助Parse从panic中恢复。

在defer函数内部,panic value被附加到错误信息中;并用err变量接收错误信息,返回给调用者。

我们也可以通过调用runtime.Stack往错误信息中添加完整的堆栈调用信息。

但是如果不加区分的恢复所有的panic异常,不是可取的做法;因为在panic之后,无法保证包级变量的状态仍然和我们预期一致。

比如,对数据结构的一次重要更新没有被完整完成、文件或者网络连接没有被关闭、获得的锁没有被释放。此外,如果写日志时产生的panic被不加区分的恢复,可能会导致漏洞被忽略。

虽然把对panic的处理都集中在一个包下,有助于简化对复杂和不可以预料问题的处理,但作为被广泛遵守的规范,你不应该试图去恢复其他包引起的panic。

公有的API应该将函数的运行失败作为error返回,而不是panic。同样的,你也不应该恢复一个由他人开发的函数引起的panic,比如说调用者传入的回调函数,因为你无法确保这样做是安全的。

因此安全的做法是有选择性的recover。换句话说,只恢复应该被恢复的panic异常,此外,这些异常所占的比例应该尽可能的低。

为了标识某个panic是否应该被恢复,我们可以将panic value设置成特殊类型。在recover时对panic value进行检查,如果发现panic value是特殊类型,就将这个panic作为errror处理,如果不是,则按照正常的panic进行处理.

还有 5W字待发布

本文,仅仅是《Golang 圣经》 的第一部分。

《Golang 圣经》后面的内容 更加精彩,涉及到高并发、分布式微服务架构、 WEB开发架构,具体请关注进展,请关注《技术自由圈》 公众号。

如果需要领取 《Golang 圣经》, 请关注《技术自由圈》 公众号,发送暗号 “领电子书” 。

最后,如果学习过程中遇到问题,可以来尼恩的 万人高并发社群中交流。

参考文献:

清华大学出版社《Java高并发核心编程 卷2 加强版》PDF

《尼恩Java面试宝典专题33:BST、AVL、RBT红黑树、三大核心数据结构(卷王专供+ 史上最全 + 2023面试必备》PDF

技术自由的实现路径 PDF:

实现你的 架构自由:

吃透8图1模板,人人可以做架构

10Wqps评论中台,如何架构?B站是这么做的!!!

阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了

峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?

100亿级订单怎么调度,来一个大厂的极品方案

2个大厂 100亿级 超大流量 红包 架构方案

… 更多架构文章,正在添加中

实现你的 响应式 自由:

响应式圣经:10W字,实现Spring响应式编程自由

这是老版本 《Flux、Mono、Reactor 实战(史上最全)

实现你的 spring cloud 自由:

Spring cloud Alibaba 学习圣经

分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)

实现你的 linux 自由:

Linux命令大全:2W多字,一次实现Linux自由

实现你的 网络 自由:

TCP协议详解 (史上最全)

网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!

实现你的 分布式锁 自由:

Redis分布式锁(图解 - 秒懂 - 史上最全)

" Zookeeper Distributed Lock - Diagram - Second Understanding "

Realize your king component freedom:

" King of the Queue: Disruptor Principles, Architecture, and Source Code Penetration "

" The King of Cache: Caffeine Source Code, Architecture, and Principles (the most complete in history, 10W super long text) "

" The King of Cache: The Use of Caffeine (The Most Complete in History) "

" Java Agent probe, bytecode enhanced ByteBuddy (the most complete in history) "

Realize your interview questions freely:

4000 pages of "Nin's Java Interview Collection" 40 topics

Please go to the following "Technical Freedom Circle" official account to get the PDF file update of the above Nien architecture notes and interview questions↓↓↓

Guess you like

Origin blog.csdn.net/crazymakercircle/article/details/130803296