Golang(1)——新人手册

370 阅读14分钟

Golang——登堂入室!

前言

由于实习的时候需要使用Go,之前用的一直是C++,抽点时间学一学这个语言。

基本的学习路线:环境安装、语法概览、典例编写、小型项目分析。

环境安装

go语言安装可以参考这个博客:

www.runoob.com/go/go-envir…

想使用IDE的话可以安装Goland,或者是VSCode。

安装完毕之后,就是一件具有仪式性的事情了——helloworld!

img

PS:为了便于便于学习和复习,建议单独建一个Go语言的文件夹,而后每一个练习项目代码都在其中分别新建一个,哪怕练习项目只有一个文件,养成良好的习惯,例如这样:

img

语法概览

关于教程,笔者使用的是微软官方所写的一个简单教程,见下:

docs.microsoft.com/zh-cn/learn…

话说初见Go的这个helloworld程序,长得真的好像java...:

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

让我来一步一步揭开它的面纱吧!

变量、函数、包

变量的声明、初始化

跟C语言不太一样的是,Go在声明变量时统一使用var关键字,并在变量名之后接数据类型。如下:

var firstName string

PS:数据类型学过其他语言的应该都了解,之后会慢慢说;

若想同时声明多个变量,可以这样:

var firstName, lastName string
var age int

也可以这样:

var (
    firstName, lastName string
    age int
)

若想在声明的时候将变量初始化,那就跟在后面即可:

var (
    firstName string = "John"
    lastName  string = "Doe"
    age       int    = 32
)

同时,在初始化变量的时候也可以不跟数据类型,Go编译器会自动推断,如下:

var (
    firstName = "John"
    lastName  = "Doe"
    age       = 32
)

或:

var (
    firstName, lastName, age = "John", "Doe", 32
)

也就是说Go的变量在声明和初始化的时候用逗号隔开即可。

重点来了,Go还有一个方便的地方在于,对于新变量,可以这样声明和初始化

firstName, lastName := "John", "Doe"
age := 32  

即用冒号等号来直接声明和初始化变量。

注1:不能对已有的变量执行冒号等于;

注2:Go语言中变量声明之后必须使用,否则会报错;常量则可以不使用

注3:变量用var,常量用const关键字(方法跟var一样),const中有一个概念叫iota,可以简单理解为存在一个常量表,iota即是下标,详见

数据类型

Go有四种数据类型:

  • 基本类型:数字、字符串和布尔值
  • 聚合类型:数组和结构
  • 引用类型:指针、切片、映射、函数和通道
  • 接口类型:接口

这里只将基本类型,其他三种之后。

**整形int:**分为有/无符号数的8/16/32/64位整数(无符号加u)。

值得注意的是,在Go中在进行类型转换的时候,必须显式转换

:若只写int,32位机上为32位,64位机为64位;

:rune 只是 int32 数据类型的别名。 它用于表示 Unicode 字符;

浮点数型float:有float32/float64,数字范围不同。

**注:**go中变量名字可以和一些关键字重复!(不建议)

img

**布尔型bool:**true/false,且不能够跟0/1隐式转换;

字符串string:双引号表示字符串,单引号表示字符,常用的转移字符如下:

img

注:所有变量声明时都有默认值,0、0.000e+000、false、空;

显式类型转换:在变量前加数据类型即可,另外还可以使用strconv包,调用其中的函数完成;

函数

函数的定义方式如下:

func name(parameters) (results) {
    body-content
}

具体一点:

func sum(number1 string, number2 string) int {
    int1, _ := strconv.Atoi(number1)
    int2, _ := strconv.Atoi(number2)
    return int1 + int2
}

即是func+函数名+形参(借用C++的概念)+返回值的类型+{函数体}

返回值可以是多个,还可以命名,主要是为了在函数中使用,见下:

func calc(number1 string, number2 string) (sum int, mul int) {
    int1, _ := strconv.Atoi(number1)
    int2, _ := strconv.Atoi(number2)
    sum = int1 + int2
    mul = int1 * int2
    return
}

(重点)如果想实现在函数中修改函数传递的变量(即函数内修改形参来更改原变量的值),需要使用 指针 没错,就是C语言那个指针(指针是包含另一个变量的内存地址的变量。),见下:

package main

func main() {
    firstName := "John"
    updateName(&firstName)
    println(firstName)
}

func updateName(name *string) {
    *name = "David"
}

**注:**如果在调用该函数时,不想使用其中某个返回值,可使用_占位符

模块与包

最开始的时候go是以.go来管理项目,没有成熟的管理体制。在Go1.12之后终于有了模块这个概念。如以下这个文件架构:

img

calculator文件夹内是一个模块(mod),其内的所有.go源文件都属于这个包,如minus和sum中都会有package calculator,见下:

img

模块的建立需要在对应的文件夹内执行:

go mod init modulename

当想在main包中引用某个模块时,可以在go.mod中进行引用:

img

关于包和模块等进一步了解,请参考

**注:**包可以看成是C++中的类,**但它没有public、private来定义公有私有变量,是通过大小写来的:**如需将某些内容设为专用内容,请以小写字母开始。如需将某些内容设为公共内容,请以大写字母开始

控制流

if...else

直接看例子吧:

package main

import "fmt"

func givemeanumber() int {
    return -1
}

func main() {
    if num := givemeanumber(); num < 0 {
        fmt.Println(num, "is negative")
    } else if num < 10 {
        fmt.Println(num, "has only one digit")
    } else {
        fmt.Println(num, "has multiple digits")
    }
}

跟C++的区别就在于不需要加括号咯。

switch

简单版本:

switch i {
case 0:
    fmt.Print("zero...")
case 1:
    fmt.Print("one...")
case 2:
    fmt.Print("two...")
default:
    fmt.Print("no match...")
}

进阶版本(匹配多个中的一个):

package main

import "fmt"

func location(city string) (string, string) {
    var region string
    var continent string
    switch city {
    case "Delhi", "Hyderabad", "Mumbai", "Chennai", "Kochi":
        region, continent = "India", "Asia"
    case "Lafayette", "Louisville", "Boulder":
        region, continent = "Colorado", "USA"
    case "Irvine", "Los Angeles", "San Diego":
        region, continent = "California", "USA"
    default:
        region, continent = "Unknown", "Unknown"
    }
    return region, continent
}
func main() {
    region, continent := location("Irvine")
    fmt.Printf("John works in %s, %s\n", region, continent)
}

再进阶(甚至可以调用函数switch和case字段都可以):

switch {
    case email.MatchString(contact):
        fmt.Println(contact, "is an email")
    case phone.MatchString(contact):
        fmt.Println(contact, "is a phone number")
    default:
        fmt.Println(contact, "is not recognized")
}

等等...似乎switch后面没有接变量。没错,switch之后可以省略,表示直接进case语句。

**注:**go的switch case会自动跳出,而不需要break,如果跟着****fallthrough关键字,就会继续执行匹配的case之后的语句

for

简单模式:

func main() {
    sum := 0
    for i := 1; i <= 100; i++ {
        sum += i
    }
    fmt.Println("sum of 1..100 is", sum)
}

其实,前处理和后处理是可选的

func main() {
    sum := 0
    for  sum <= 100; {
        sum += i
    }
    fmt.Println("sum of 1..100 is", sum)
}

无限循环模式(等同于while),利用break弹出:

for {
    fmt.Print("Writting inside the loop...")
    if num = rand.Int31n(10); num == 5 {
        fmt.Println("finish!")
        break
    }
    fmt.Println(num)
}

一样有continue关键字:

func main() {
    sum := 0
    for num := 1; num <= 100; num++ {
        if num%5 == 0 {
            continue
        }
        sum += num
    }
    fmt.Println("The sum by 5, is", sum)
}

defer

这个函数做的事情跟它的意思一样“推迟”:defer 语句会推迟函数(包括任何参数)的运行,直到包含 defer 语句的函数(大括号)完成。

defer会将其之后的函数或语句压入类似栈的数据结构,而后逐一弹出执行,如下面这个例子:

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

先想想如果没有defer,结果是什么样子的?如果有就是下面:

img

可见,defer其实就是一个栈型结构,它会存储对应的函数参数。如果存在多个defer语句,它们之间也是按照“后来居上”的,如下:

img

通常情况下,当你想要避免忘记任务(例如关闭文件或运行清理进程)时,可以推迟defer某个函数的运行。

panic

这个函数有点类似于C++的异常机制,panic可以强制程序进入紧急状态,此时控制流完全中断,defer的函数执行输出,而后程序异常退出并打印堆栈信息。

package main

import "fmt"

func main() {
    g(0)
    fmt.Println("Program finished successfully!")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic("Panic in g() (major)")
    }
    defer fmt.Println("Defer in g()", i)
    fmt.Println("Printing in g()", i)
    g(i + 1)
}

结果:

img

recover

当主动或被动(程序出错)调用panic时,程序通常会崩溃。有时,可能想要避免程序崩溃,改为在内部报告错误。或者,可能想要关闭与某个资源的连接,以免出现更多问题。这时就需要使用recover了,它可以使程序在出现紧急状态时重新获得控制权它只能在推迟的函数中使用,未出现紧急状态,recover()函数返回nil,无意义;见下例子:

package main

import "fmt"

func main() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered in main", r)
        }
    }()
    g(0)
    fmt.Println("Program finished successfully!")
}

func g(i int) {
    if i > 3 {
        fmt.Println("Panicking!")
        panic("Panic in g() (major)")
    }
    defer fmt.Println("Defer in g()", i)
    fmt.Println("Printing in g()", i)
    g(i + 1)
}

结果为:

img

此时程序“看起来是正常结束”,因为没有堆栈信息了。利用panic和recover,go实现了异常检测和处理。

聚合类型

数组

声明的方式跟C++很类似:

var a [3]int

若是想初始化,有这么几种方式:

  • 默认初始化

如果不主动初始化,按照数据类型来对数组中的元素进行初始化;

  • 手动初始化

cities := [5]string{"New York", "Paris", "Berlin", "Madrid"}

  • 省略号方式

在不知道需要数组多长的时候,可以用省略指代:

q := [...]int{1, 2, 3}

  • 省略加特定

可以为指定位置的数组元素进行初始化:

numbers := [...]int{99: -1}

多维数组暂不赘述;

切片

切片只是名为基础数组的数组之上的一种数据结构。 通过切片,可访问整个基础数组,也可仅访问部分元素。

切片只有 3 个组件

  • 指针,指向基础数组可访问的第一个元素(并非一定是数组的第一个元素)
  • 长度,指示切片中的元素数目。
  • 容量,显示切片开头与基础数组结束之间的元素数目

如下图;

img

其实,切片只是基础数组的一个子集,截取了数组的某一个部分

它的声明可以通过以下几种方式:

months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"} 
quarter1 := months[0:3] 
quarter2 := months[3:6] 
quarter3 := months[6:9] 
quarter4 := months[9:12]

注:在声明切片时,它的len和cap是不一样的,cap是根据当前数组所拥有的最长长度而定;

利用append来对切片进行添加;

cap是翻倍增长的;(会在一个新建数组上进行,而不是基础数组)

删除切片中某一元素,可以利用数组下标,而后缩短切片来实现,例如:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

    fmt.Println("Before", letters)

    letters[remove] = letters[len(letters)-1]
    letters = letters[:len(letters)-1]

    fmt.Println("After", letters)
}

还可以利用切片副本来处理:

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

因为切片和基础数组是绑定在一起的,**修改切片会影响基础数组,修改基础数组会影响切片,实际场景下可能不想要相符影响,那就需要副本了,**见下:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

其结果为:

img

映射(哈希表)

声明映射的方式:

studentsAge := map[string]int{ "john": 32, "bob": 31, }
//等同于
var studentsAge map[string]int
//以上被称为nil映射,下列被称为make映射
studentsAge := make(map[string]int)

如何添加?(映射是动态的)

package main

import "fmt"

func main() {
    studentsAge := make(map[string]int)
    studentsAge["john"] = 32
    studentsAge["bob"] = 31
    fmt.Println(studentsAge)
}

直接添加即可(相当于已经进行了初始化),如果是nil映射会报错,加入不进去

如何访问?

可以使用下标法,而且还能判断是否存在!

package main

import "fmt"

func main() {
    studentsAge := make(map[string]int)
    studentsAge["john"] = 32
    studentsAge["bob"] = 31

    age, exist := studentsAge["christy"]
    if exist {
        fmt.Println("Christy's age is", age)
    } else {
        fmt.Println("Christy's age couldn't be found")
    }
}

如何删除?

——调用delete即可。

 delete(studentsAge, "john")

若对一个不存在的键值对进行删除,不会报错(反正也没影响)。

结构体

定义和初始化直接看代码:

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}
//初始化(按照数据结构及顺序来)
employee := Employee{1001, "John", "Doe", "Doe's Street"}
//or
employee := Employee{LastName: "Doe", FirstName: "John"}

如果要进行结构体的嵌入,有两种方式:

type Employee struct {
    Information Person
    ManagerID   int
}
//or
type Employee struct {
    Person(直接嵌入)
    ManagerID int
}

Go语言在标准库中深度集成了json,此处暂略,进一步了解可参考

方法与接口

方法

声明

声明的方式如下:

func (variable type) MethodName(parameters ...) {
    // method functionality
}

其中variable是类的一个对象,type就是一个类名(结构名)。

如果想实现对原本的类对象进行修改(一般调用函数是对类对象的副本进行操作的)就得借助指针:

func (t *triangle) doubleSize() {
    t.size *= 2
}

**注:**只能对当前包内的对象添加方法,不能乱来;

如果对其他类型进行了嵌套,那么可以直接调用其他类型的方法,如下:

type coloredTriangle struct {
    triangle
    color string
}

func main() {
    t := coloredTriangle{triangle{3}, "blue"}
    fmt.Println("Size:", t.size)
    fmt.Println("Perimeter", t.perimeter())
}

注意到了么,嵌套的方法可以直接调用,但是之前说嵌套的成员变量只能够逐级调用。其实是Go语言利用一个包装器模式来帮我们进行了自动推广,类似于:

func (t coloredTriangle) perimeter() int {
    return t.triangle.perimeter()
}

重载

Go语言的方法也是可以重载的,相当于子类重写了父类(但其实两者间并不是父子关系),只需要在类中重新实现函数即可,如下:

func (t coloredTriangle) perimeter() int {
    return t.size * 3 * 2
}

但是,函数重载之后,如果想调用“子类”中的方法,就不能直接调用了,必须通过具体的对象逐级显式调用

封装

封装,即是C++中的public或者private之类的,限制类外的代码对类内属性的访问权限。

之前也提到过,Go语言是通过方法名的大小写来实现的:

大写即是Public;

小写即是Private;

接口

接口是指规定一定的实现形式(如函数名、参数、返回等),而具体实现应根据实际情况而定。而且,接口规定的方法必须实现。在go语言中,只要你实现了对应的接口方法,就是实现了接口(而不用再单独设置关键字,Go会自动判定)

声明

如规定了两个接口:

type Shape interface {
    Perimeter() float64
    Area() float64
}

那么任何shape类型都要对其进行实现。

实现

接着上面的那个shape例子,如果我想实现一个shape类型,那么就必须进行接口方法的实现:

type Square struct {
    size float64
}

func (s Square) Area() float64 {
    return s.size * s.size
}

func (s Square) Perimeter() float64 {
    return s.size * 4
}

扩展实现

我们可以通过下面这种方式扩展对应方法的自定义版本:

type Stringer interface {
    String() string
}

package main

import "fmt"

type Person struct {
    Name, Country string
}

func (p Person) String() string {
    return fmt.Sprintf("%v is from %v", p.Name, p.Country)
}
func main() {
    rs := Person{"John Doe", "USA"}
    ab := Person{"Mark Collins", "United Kingdom"}
    fmt.Printf("%s\n%s\n", rs, ab)
}

有关接口的进一步了解,可以参考

并发

Go之所以如此火爆,其中一个关键就在于它在并发上无与伦比的性能!Go的并发跟C++的并发不一样,让我们一起来看看。

——这里只介绍Go的goroutine(轻量级线程)方式,传统的依靠共享内存的线程并发方式不做介绍。

Goroutine

怎么并发一下? go一下就可以了。

func main(){
    login()
    go launch()
}

也许更喜欢匿名函数?

func main(){
    login()
    go func() {
        launch()
    }()//这个()是为了实现匿名函数的调用
}

注意,协程(routine)的并发跟线程并发一样,多个线程/协程之间一般是不会等待对方运行完毕之后总程序才停止,而是多个同时运行,主协程运行完毕之后就提示程序已运行完,如果想实现协程间通信,见后。

channel

go语言不是通过共享内存,而是通过通信通道实现内存共享的!

channel是协程之间传递值的方式,如何创建?

ch := make(chan int)
close(ch)

注意,go的channel是分为有缓冲和无缓冲的,使用make默认是无缓冲的。这有什么区别呢?

无缓冲的channel只有当有接收方的时候,发送方才算真正的发送的

有缓冲的channel类似于一个消息队列,在创建时指定大小即可:

ch := make(chan string, 10)

无缓冲channel同步通信。它们保证每次发送数据时,程序都会被阻止,直到有人从channel中读取数据。

相反,有缓冲channel将发送和接收操作解耦,相当于异步操作。它们不会阻止程序,但必须小心使用,因为可能最终会导致死锁。

如何读取和发送呢?

ch <- x // sends (or write) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded

另外,在函数参数中可以对channel的方向进行定义(即只能读取/接收),避免误用,如果误用会编译错误。

package main

import "fmt"

func send(ch chan<- string, message string) {
    fmt.Printf("Sending: %#v\n", message)
    ch <- message
}

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
    ch := make(chan string, 1)
    send(ch, "Hello World!")
    read(ch)
}

select

select关键字可以用来与多个channel交互,例如等待事件发生之类的,有点类似于switch。见下:

package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Done processing!"
}

func replicate(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Done replicating!"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go process(ch1)
    go replicate(ch2)

    for i := 0; i < 2; i++ {
        select {
        case process := <-ch1:
            fmt.Println(process)
        case replicate := <-ch2:
            fmt.Println(replicate)
        }
    }
}

结果是什么?

img

go中的select类似于网络编程中的多路复用,它会阻塞主程序,等待其候选项“case”的事件就绪。

错误处理及日志

错误处理

panic 和 recover 之类的内置函数来管理程序中的异常或意外行为。但错误是已知的失败,程序应该可以处理它们。

GO语言中错误的处理方式只需要if和return即可,如下述代码:

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}

通过检查返回值中是否有nil,来确定调用这个函数是否有误。

其实现原理是对调用函数内,递归的判断是否出错,这就有两种应对错误的方式,一是直接放回错误,如下:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

二是,添加重试策略如下:

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

日志系统

log包提供了基本的日志信息,但是不提供日志级别,如下:

import (
    "log"
)

func main() {
    log.Print("Hey, I'm a log!")
}

它会默认添加时间:

img

除此之外,

log.Fatal() 函数记录错误并结束程序;

log.Panic()函数同上,并输出堆栈信息;

log.SetPrefix()函数可让日志消息添加前缀(它之后的);

log.SetOutPut()函数可以设置输出到文件中;

至于其他的库:logrus、zerolog、zap、Apex,请百度谷歌相关知识。

参考资料

docs.microsoft.com/zh-cn/learn…