Golang学习之路:从新手到专家

发表时间: 2020-04-10 12:17


Pic: Gopher mascot and old logo


让我们从Go(或Golang)的简短介绍开始。 Go是由Google工程师Robert Griesemer,Rob Pike和Ken Thompson设计的。 它是一种静态类型的编译语言。 第一个版本于2012年3月作为开源发布。

" Go是一种开放源代码编程语言,可轻松构建简单,可靠且高效的软件"。

— GoLang

在许多语言中,有很多方法可以解决给定的问题。 程序员可能会花费大量时间来思考解决问题的最佳方法。

另一方面,Go相信更少的功能-只有一种正确的方法来解决问题。

这样可以节省开发人员时间,并使大型代码库易于维护。 Go中没有诸如地图和过滤器之类的"表达性"功能。

"当您具有增加表达力的功能时,通常会增加费用"

—罗伯·派克

Recently published new logo of go lang:

入门

Go由包组成。 包主体告诉Go编译器该程序被编译为可执行文件,而不是共享库。 它是应用程序的入口点。 主程序包定义为:

package main


让我们通过在Go工作区中创建一个文件main.go来编写一个简单的hello world示例。

工作空间

Go中的工作空间由环境变量GOPATH定义。

您编写的任何代码均应写入工作空间内。 Go将搜索GOPATH目录或GOROOT目录(在安装Go时默认设置)中的所有软件包。 GOROOT是安装go的路径。

将GOPATH设置为所需的目录。 现在,让我们将其添加到〜/ workspace文件夹中。

# export envexport GOPATH=~/workspace# go inside the workspace directorycd ~/workspace

在我们刚刚创建的工作空间文件夹中,使用以下代码创建文件main.go。

你好,世界!

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

在上面的示例中,fmt是Go中的内置程序包,它实现用于格式化I / O的功能。

我们使用import关键字在Go中导入包。 func main是执行代码的主要入口点。 Println是fmt软件包中的一个函数,可为我们打印" hello world"。

让我们看看运行此文件。 我们可以通过两种方式运行Go命令。 众所周知,Go是一种编译语言,因此我们首先需要对其进行编译,然后再执行。

> go build main.go


这将创建一个二进制可执行文件main,现在我们可以运行它:

> ./main # Hello World!


还有另一种更简单的方法来运行程序。 go run命令有助于抽象编译步骤。 您只需运行以下命令即可执行该程序。

go run main.go # Hello World!


注意:要试用此博客中提到的代码,可以使用

变量

Go中的变量被明确声明。 Go是一种静态类型的语言。 这意味着在变量声明时检查变量类型。 变量可以声明为:

var a int


在这种情况下,该值将设置为0。使用以下语法声明和初始化具有不同值的变量:

var a = 1


此处,变量自动分配为int。 我们可以为变量声明使用简写定义:

message := "hello world"


我们还可以在同一行中声明多个变量:

var b, c int = 2, 3


资料类型

像任何其他编程语言一样,Go支持各种不同的数据结构。 让我们探索其中的一些:

数字,字符串和布尔值

一些受支持的数字存储类型为int,int8,int16,int32,int64,uint,uint8,uint16,uint32,uint64,uintptr…

字符串类型存储字节序列。 它用关键字字符串表示和声明。

使用关键字bool存储布尔值。

Go还支持复数类型数据类型,可以使用complex64和complex128进行声明。

var a bool = truevar b int = 1var c string = 'hello world'var d float32 = 1.222var x complex128 = cmplx.Sqrt(-5 + 12i)


数组,切片和Map

数组是具有相同数据类型的元素序列。 数组在声明时定义了固定的长度,因此不能扩展得更多。 数组声明为:

var a [5]int


数组也可以是多维的。 我们可以简单地使用以下格式创建它们:

var multiD [2][3]int


对于运行时数组值更改的情况,数组是有限制的。 数组也不提供获取子数组的能力。 为此,Go具有称为切片的数据类型。

切片存储一系列元素,并且可以随时扩展。 切片声明类似于数组声明-未定义容量:

var b []int


这将创建零容量和零长度的切片。 切片也可以用容量和长度来定义。 我们可以使用以下语法:

numbers := make([]int,5,10)


在此,切片的初始长度为5,容量为10。

切片是对数组的抽象。 切片使用数组作为基础结构。 切片包含三个组成部分:容量,长度和指向基础数组的指针,如下图所示:

image src:


可以通过使用附加或复制功能来增加切片的容量。 附加函数将值添加到数组的末尾,并在需要时增加容量。

numbers = append(numbers, 1, 2, 3, 4)


增加切片容量的另一种方法是使用复制功能。 只需创建另一个更大容量的切片并将原始切片复制到新创建的切片即可:

// create a new slicenumber2 := make([]int, 15)// copy the original slice to new slicecopy(number2, number)


我们可以创建切片的子切片。 只需使用以下命令即可完成此操作:

// initialize a slice with 4 len and valuesnumber2 = []int{1,2,3,4}fmt.Println(numbers) // -> [1 2 3 4]// create sub slicesslice1 := number2[2:]fmt.Println(slice1) // -> [3 4]slice2 := number2[:3]fmt.Println(slice2) // -> [1 2 3]slice3 := number2[1:4]fmt.Println(slice3) // -> [2 3 4]


Map是Go中的一种数据类型,它将键映射到值。 我们可以使用以下命令定义地图:

var m map[string]int


这里m是新的map变量,其键为字符串,值是整数。 我们可以轻松地向地图添加键和值:

// adding key/valuem['clearity'] = 2m['simplicity'] = 3// printing the valuesfmt.Println(m['clearity']) // -> 2fmt.Println(m['simplicity']) // -> 3

类型转换

可以使用类型转换将一种类型的数据类型转换为另一种类型。 让我们看一个简单的类型转换:

a := 1.1b := int(a)fmt.Println(b)//-> 1


并非所有类型的数据类型都可以转换为另一种类型。 确保数据类型与转换兼容。

条件语句

if/else

对于条件语句,我们可以使用if-else语句,如下例所示。 确保花括号与条件在同一行。

if num := 9; num < 0 {	fmt.Println(num, "is negative")} else if num < 10 {	fmt.Println(num, "has 1 digit")} else {	fmt.Println(num, "has multiple digits")}


Switch/Case

切换案例有助于组织多个条件语句。 以下示例显示了一个简单的switch case语句:

i := 2switch i {  case 1:  	fmt.Println("one")  case 2:  	fmt.Println("two")  default:  	fmt.Println("none")}


for loop

Go具有用于循环的单个关键字。 单个for循环命令可帮助实现各种循环:

i := 0sum := 0for i < 10 {  sum += 1  i++}fmt.Println(sum)


上面的示例类似于C中的while循环。相同的for语句可用于普通的for循环

sum := 0for i := 0; i < 10; i++ {	sum += i}fmt.Println(sum)


Go中的无限循环:

for {}


指针

Go提供了指针。 指针是存放值地址的地方。 指针由*定义。 根据数据类型定义指针。 例:

var ap *int


ap是指向整数类型的指针。 &运算符可用于获取变量的地址。

a := 12ap = &a


指针指向的值可以使用*运算符进行访问:

fmt.Println(*ap)// => 12


在将结构作为参数传递或为定义的类型声明方法时,通常首选指针。

· 传递值时,实际上会复制该值,这意味着需要更多内存

· 传递指针后,函数更改的值会在方法/函数调用器中反映出来。

例:

func increment(i *int) {	*i++}func main() {  i := 10  increment(&i)  fmt.Println(i)}//=> 11


注意:在博客中尝试示例代码时,请不要忘记将其包含在main软件包中,并在需要时导入fmt或其他软件包,如上面的第一个main.go示例所示。

函数

在主包中定义的主要功能是执行go程序的入口。 可以定义和使用更多函数。 让我们看一个简单的例子:

func add(a int, b int) int {  c := a + b  return c}func main() {	fmt.Println(add(2, 1))}//=> 3


如上例所示,使用func关键字后跟函数名称定义了Go函数。 需要根据函数的数据类型定义函数的参数,最后根据返回值的数据类型进行定义。

函数的返回也可以在函数中预定义:

func add(a int, b int) (c int) {  c = a + b  return}func main() {	fmt.Println(add(2, 1))}//=> 3


这里c被定义为返回变量。 因此,定义的变量c将自动返回,而无需在最后的return语句中定义。

您也可以从单个函数中返回多个返回值,这些函数用逗号分隔返回值。

func add(a int, b int) (int, string) {  c := a + b  return c, "successfully added"}func main() {  sum, message := add(2, 1)  fmt.Println(message)  fmt.Println(sum)}


方法,结构和接口

Go并不是一种完全面向对象的语言,但是具有结构,接口和方法,它具有很多面向对象的支持和感觉。

结构

结构是不同字段的类型化集合。 结构用于将数据分组在一起。 例如,如果我们要对Person类型的数据进行分组,则定义一个person的属性,其中可以包括姓名,年龄,性别。 可以使用以下语法定义结构:

type person struct {  name string  age int  gender string}


定义了人员类型结构后,现在创建一个人员:

//way 1: specifying attribute and valuep = person{name: "Bob", age: 42, gender: "Male"}//way 2: specifying only valueperson{"Bob", 42, "Male"}


我们可以使用点(.)轻松访问这些数据。

p.name//=> Bobp.age//=> 42p.gender//=> Male


您还可以直接通过结构体的指针访问结构体的属性:

pp = &person{name: "Bob", age: 42, gender: "Male"}pp.name//=> Bob


方法

方法是带有接收器的一种特殊功能。 接收者可以是值或指针。 让我们创建一个名为describe的方法,该方法具有在上面的示例中创建的接收者类型的人员:

package mainimport "fmt"// struct definationtype person struct {  name string  age int  gender string}// method definationfunc (p *person) describe() {	fmt.Printf("%v is %v years old.", p.name, p.age)}func (p *person) setAge(age int) {	p.age = age}func (p person) setName(name string) {	p.name = name}func main() {  pp := &person{name: "Bob", age: 42, gender: "Male"}  pp.describe()  // => Bob is 42 years old  pp.setAge(45)  fmt.Println(pp.age)  //=> 45  pp.setName("Hari")  fmt.Println(pp.name)  //=> Bob}


正如我们在上面的示例中看到的那样,现在可以使用点运算符pp.describe调用该方法。 注意,接收者是一个指针。 通过指针,我们传递了对该值的引用,因此,如果对方法进行任何更改,它将反映在接收器pp中。它也不会创建该对象的新副本,从而节省了内存。

请注意,在上面的示例中,age的值已更改,而name的值未更改,因为方法setName是接收者类型,而setAge是指针类型。

介面

Go接口是方法的集合。 接口可将类型的属性组合在一起。 让我们以界面动物为例:

type animal interface {	description() string}


这里的动物是接口类型。 现在,让我们创建2种不同类型的动物,它们实现动物接口类型:

package mainimport (	"fmt")type animal interface {	description() string}type cat struct {  Type string  Sound string}type snake struct {  Type string  Poisonous bool}func (s snake) description() string {	return fmt.Sprintf("Poisonous: %v", s.Poisonous)}func (c cat) description() string {	return fmt.Sprintf("Sound: %v", c.Sound)}func main() {  var a animal  a = snake{Poisonous: true}  fmt.Println(a.description())  a = cat{Sound: "Meow!!!"}  fmt.Println(a.description())}//=> Poisonous: true//=> Sound: Meow!!!


在main函数中,我们创建一个animal类型的变量a。 我们为动物指定蛇和猫的类型,并使用Println打印a.description。 由于我们以不同的方式实现了在两种类型(猫和蛇)中描述的方法,因此我们获得了对动物的描述。

配套

我们将所有代码都包装在Go中。 主程序包是程序执行的入口点。 Go中有很多内置软件包。 我们一直使用的最著名的是fmt软件包。

" go软件包提供了大型编程的主要机制中的Go软件包,它们使将大型项目分成较小的部分成为可能。"—罗伯特·格里塞默尔

安装套件

go get <package-url-github>// examplego get github.com/satori/go.uuid


我们安装的软件包保存在我们的工作目录GOPATH env中。 您可以通过进入工作目录cd $ GOPATH / pkg中的pkg文件夹来查看软件包。

创建一个自定义包

首先创建一个文件夹custom_package:

> mkdir custom_package> cd custom_package


要创建自定义程序包,我们需要首先创建一个具有所需程序包名称的文件夹。 假设我们正在建立一个打包人员。 为此,我们在custom_package文件夹中创建一个名为person的文件夹:

> mkdir person> cd person


现在,在此文件夹中创建一个文件person.go。

package personfunc Description(name string) string {	return "The person name is: " + name}func secretName(name string) string {	return "Do not share"}

现在,我们需要安装该软件包,以便可以导入和使用它。 因此,让我们安装它:

> go install


现在,我们回到custom_package文件夹并创建一个main.go文件

package mainimport(  "custom_package/person"  "fmt")func main(){  p := person.Description("Milap")  fmt.Println(p)}// => The person name is: Milap


现在,我们可以在这里导入创建的包人并使用功能描述。 请注意,将无法访问我们在软件包中创建的函数secretName。 在Go中,以大写字母开头的方法名称将是私有的。

包文件

Go内置了对软件包文档的支持。 运行以下命令以生成文档:godoc person Description

这将为我们的打包人员生成Description函数的文档。 要查看文档,请使用以下命令运行Web服务器:

godoc -http=":8080"


现在转到URL http:// localhost:8080 / pkg /并查看我们刚刚创建的软件包的文档。

Go中的一些内置软件包

fmt

该软件包实现格式化的I / O功能。 我们已经使用该软件包将其打印输出到stdout。

json

Go中另一个有用的软件包是json软件包。 这有助于对JSON进行编码/解码。 让我们以一个示例来对一些json进行编码/解码:

编码

package mainimport (  "fmt"  "encoding/json")func main(){  mapA := map[string]int{"apple": 5, "lettuce": 7}  mapB, _ := json.Marshal(mapA)  fmt.Println(string(mapB))}


解码

package mainimport (  "fmt"  "encoding/json")type response struct {  PageNumber int `json:"page"`  Fruits []string `json:"fruits"`}func main(){  str := `{"page": 1, "fruits": ["apple", "peach"]}`  res := response{}  json.Unmarshal([]byte(str), &res)  fmt.Println(res.PageNumber)}//=> 1


当使用unmarshal解码json字节时,第一个参数是json字节,第二个参数是我们想要将json映射到的响应类型struct的地址。 请注意,json:" page"将页面密钥映射到结构中的PageNumber密钥。

错误处理

错误是程序的意外结果。 假设我们正在对外部服务进行API调用。 此API调用可能成功或失败。 存在错误类型时,可以识别Go程序中的错误。 让我们来看一个例子:

resp, err := http.Get("http://example.com/")


在这里,对错误对象的API调用可能通过或失败。 我们可以检查错误是否为nil或存在,并相应地处理响应:

package mainimport (  "fmt"  "net/http")func main(){  resp, err := http.Get("http://example.com/")  if err != nil {    fmt.Println(err)    return  }	fmt.Println(resp)}


从函数返回自定义错误

当我们编写自己的函数时,有时会出现错误。 这些错误可以在错误对象的帮助下返回:

func Increment(n int) (int, error) {  if n < 0 {    // return error object    return nil, errors.New("math: cannot process negative number")  }  return (n + 1), nil}func main() {  num := 5  if inc, err := Increment(num); err != nil {  	fmt.Printf("Failed Number: %v, error message: %v", num, err)  }else {  	fmt.Printf("Incremented Number: %v", inc)  }}


Go中内置的大多数程序包或我们使用的外部程序包都有错误处理机制。 因此,我们调用的任何函数都可能存在错误。 就像在上面的示例中所做的那样,这些错误绝不能忽略,并且总是在我们调用这些函数的地方进行适当地处理。

panic

panic是无法处理的,在程序执行过程中会突然遇到。 在Go中,panic不是处理程序中异常的理想方法。 建议改用错误对象。 发生panic时,程序执行将停止。 panic后被执行的事情是推迟。

defer

defer是始终在函数末尾执行的东西。

//Gopackage mainimport "fmt"func main() {  f()  fmt.Println("Returned normally from f.")}func f() {  defer func() {    if r := recover(); r != nil {    	fmt.Println("Recovered in f", r)    }  }()  fmt.Println("Calling g.")  g(0)  fmt.Println("Returned normally from g.")}func g(i int) {  if i > 3 {    fmt.Println("Panicking!")    panic(fmt.Sprintf("%v", i))  }  defer fmt.Println("Defer in g", i)  fmt.Println("Printing in g", i)  g(i + 1)}


在上面的示例中,我们使用panic()恐慌了程序的执行。 您会注意到,有一个defer语句,它将使程序在程序执行结束时执行该行。 当我们需要在函数末尾执行某些操作(例如关闭文件)时,也可以使用Defer。

并发

Go在构建时考虑了并发性。 Go中的并发可以通过轻量级线程Go例程来实现。

例行

Go例程是可以与另一个函数并行或同时运行的函数。 创建Go例程非常简单。 只需在函数前面添加关键字Go,就可以使其并行执行。 Go例程非常轻巧,因此我们可以创建数千个例程。 让我们看一个简单的例子:

package mainimport (  "fmt"  "time")func main() {  go c()  fmt.Println("I am main")  time.Sleep(time.Second * 2)}func c() {  time.Sleep(time.Second * 2)  fmt.Println("I am concurrent")}//=> I am main//=> I am concurrent

如上例所示,函数c是Go例程,与Go主线程并行执行。 有时我们想在多个线程之间共享资源。 Go倾向于不与另一个线程共享变量,因为这会增加死锁和资源等待的机会。 在Go例程之间共享资源的另一种方法是:通过go通道。

channel

我们可以使用通道在两个Go例程之间传递数据。 创建通道时,必须指定通道接收的数据类型。 让我们创建一个具有字符串类型的简单频道,如下所示:

c := make(chan string)


通过此通道,我们可以发送字符串类型的数据。 我们可以在此通道中发送和接收数据:

package mainimport "fmt"func main(){  c := make(chan string)  go func(){ c <- "hello" }()  msg := <-c  fmt.Println(msg)}//=>"hello"


接收方通道等待,直到发送方将数据发送到该通道。

单向通道

在某些情况下,我们希望Go例程通过通道接收数据但不发送数据,反之亦然。 为此,我们还可以创建一个单向通道。 让我们看一个简单的例子:

package mainimport (	"fmt")func main() {  ch := make(chan string)  go sc(ch)  fmt.Println(<-ch)}func sc(ch chan<- string) {	ch <- "hello"}


在上面的示例中,sc是Go例程,该例程只能将消息发送到通道,但不能接收消息。

使用select为Go例程组织多个通道

一个功能可能正在等待多个通道。 为此,我们可以使用select语句。 让我们看一个例子以更清楚:

package mainimport (  "fmt"  "time")func main() {  c1 := make(chan string)  c2 := make(chan string)  go speed1(c1)  go speed2(c2)  fmt.Println("The first to arrive is:")  select {    case s1 := <-c1:    	fmt.Println(s1)    case s2 := <-c2:    	fmt.Println(s2)  }}func speed1(ch chan string) {  time.Sleep(2 * time.Second)  ch <- "speed 1"}func speed2(ch chan string) {  time.Sleep(1 * time.Second)  ch <- "speed 2"}


在上面的示例中,主电源在两个通道c1和c2上等待。 使用select case语句打印主要功能,然后从该通道中首先接收的那个消息发送消息。

缓冲通道

您可以在Golang中创建一个缓冲通道。 对于缓冲的通道,如果缓冲区已满,则将阻止发送到该通道的消息。 让我们看一个例子:

package mainimport "fmt"func main(){  ch := make(chan string, 2)  ch <- "hello"  ch <- "world"  ch <- "!" // extra message in buffer  fmt.Println(<-ch)}// => fatal error: all goroutines are asleep - deadlock!

为什么Golang成功?

简单……— Rob-pike

我们了解了Go的一些主要组件和功能。

· 变量,数据类型

· 数组切片和Map

· 函数

· 循环和条件语句

· 指针

· 配套

· 方法,结构和接口

· 错误处理

· 并发-Go例程和通道

恭喜,您现在对Go有了一个不错的了解。

我最有生产力的日子之一就是扔掉1000行代码。

—肯·汤普森

不要在这里停下来。 继续前进。 考虑一个小型应用程序,然后开始构建。

(本文翻译自Milap Neupane的文章《Learning Go — from zero to hero》,参考:
https://medium.com/free-code-camp/learning-go-from-zero-to-hero-d2a3223b3d86)