后端语言很难?前端入门go基础语法只需要3小时!(下)

3,500 阅读17分钟

继续之前这两篇文章,我们讲到了接口。

接口

在 Go 中,接口是一种类型,它定义了一组方法。接口类型变量可以存储任何实现了该接口的类型的值,这样就可以在程序中使用统一的接口类型来处理不同的具体类型。

例如:

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

上面的代码定义了一个名为 Reader 的接口,该接口有一个方法 Read(p []byte) (n int, err error)。任何类型如果实现了Reader接口中的Read()方法,都可以被赋值给Reader类型变量.

接口还可以包含多个方法, 例如

type Writer interface {
    Write(p []byte) (n int, err error)
    Close() error
}

这个Writer接口包含了两个方法,Write和Close。任何类型如果实现了这两个方法,就可以被赋值给Writer类型变量。

在 Go 中,接口是隐式实现的,因此实现接口不需要显式声明。只要类型定义了接口中的所有方法,就可以认为它实现了该接口。

接口还可以嵌套, 例如

type ReadWriter interface {
    Reader
    Writer
}

这个ReadWriter接口包含了Reader和Writer两个接口中所有的方法,任何类型实现了这两个接口中所有的方法就可以被赋值给ReadWriter类型变量。

对比js:这个跟ts也差不多,但语法不太一样,我们的interface要继承或者type通过 && 来聚合,go还有点像ts中type这种组合的方式

接口的多态

Go 中,接口还可以用来实现多态。比如我们有一个函数,它接受一个接口类型的参数,那么这个函数就可以接受任何实现了这个接口的类型的参数。这样就可以在编译时就发现类型问题,而不是在运行时。

例如:

type Shape interface {
    Area() float64
}

type Rectangle struct {
    width, height float64
}

func (r Rectangle) Area() float64 {
    return r.width * r.height
}

type Circle struct {
    radius float64
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func getArea(s Shape) float64 {
    return s.Area()
}

func main() {
    r := Rectangle{width: 10, height: 5}
    c := Circle{radius: 5}

    fmt.Println("Area of rectangle: ", getArea(r))
    fmt.Println("Area of circle: ", getArea(c))
}

上面的代码定义了一个接口 Shape,它有一个方法 Area()。Rectangle 和 Circle 两种类型都实现了这个方法,所以它们都实现了 Shape 接口。

这是go中实现面向对象继承的内容,是很重要的,因为面向接口编程,是函数跟业务解耦具体业务是非常重要的,函数本身解耦了数据和行为,有了接口就把数据限制了,把数据变为一种协议,只要实现这个协议的数据就可以,从而实现面向接口实现函数

接口类型断言和类型判断

另外,Go 中的接口还支持类型断言和类型判断,可以在运行时判断一个变量是否实现了某个接口。

类型断言是指将接口类型断言成具体类型。这样就可以访问具体类型的字段和方法。类型断言的语法如下:

x.(T)

x 是一个接口类型,T 是具体类型。如果 x 断言成功,则返回 x 的具体值,否则返回一个类型断言失败的 panic。

类型判断是指判断接口类型是否实现了某个接口。这样就可以在运行时判断一个变量是否实现了某个接口。类型判断的语法如下:

x.(type)

x 是一个接口类型。如果 x 实现了 T 接口,返回x的具体类型,否则返回nil

在 Go 中,类型断言和类型判断可以用来在运行时判断一个变量是否实现了某个接口,并访问具体的字段和方法。这样可以提高代码的灵活性。

我们举例子说明一下:

类型断言:

package main

import "fmt"

type animal interface {
    speak()
}

type dog struct {
}

func (d dog) speak() {
    fmt.Println("Woof!")
}

func main() {
    var d animal = dog{}
    if val, ok := d.(dog); ok {
        val.speak()
    } else {
        fmt.Println("d is not a dog")
    }
}

上面的代码中,我们定义了一个 animal 接口,并实现了一个 dog 类型。在 main 函数中,我们声明了一个 animal 类型的变量 d,并将一个 dog 类型的变量赋值给它。然后我们使用类型断言来判断 d 是否是 dog 类型。如果断言成功,就可以调用 dog 类型的 speak 方法。

注意:这里我们需要注意的是,断言是会返回内容的,这个跟typescript是完全不一样的

类型判断:

package main

import "fmt"

type animal interface {
    speak()
}

type dog struct {
}

func (d dog) speak() {
    fmt.Println("Woof!")
}

func main() {
    var d animal = dog{}
    switch v := d.(type) {
        case dog:
            v.speak()
        default:
            fmt.Println("d is not a dog")
    }
}

我们定义了一个 animal 接口,并实现了一个 dog 类型。在 main 函数中,我们声明了一个 animal 类型的变量 d,并将一个 dog 类型的变量赋值给它。然后我们使用类型判断来判断 d 是否实现了 dog 类型。如果判断成功,就可以调用 dog 类型的 speak 方法。

总结:类型断言和类型判断都可以用来在运行时判断一个变量是否实现了某个接口,并访问具体的字段和方法。但是类型断言是访问具体类型的值,类型判断是访问具体类型。

对比js,我们的ts好像没有直接把类型变为字符串的能力,毕竟js不是内置类型的,打通js和类型。

Goroutines和Channels

并发程序指同时进行多个任务的程序,随着硬件的发展,并发程序变得越来越重要。Web服务器会一次处理成千上万的请求。平板电脑和手机app在渲染用户画面同时还会后台执行各种计算任务和网络请求。即使是传统的批处理问题--读取数据,计算,写输出--现在也会用并发来隐藏掉I/O的操作延迟以充分利用现代计算机设备的多个核心。计算机的性能每年都在以非线性的速度增长。

go的Goroutines可以类比以下协程,但是它们是有明显区别。

在这里很多前端同学对操作系统中,进程、线程、协程并不了解,我们有必要介绍一下:

一定注意,这些是大概念,具体到实现的语言里,是有区别的 。

进程(Process),线程(Thread),[协程]

  • 进程:

一个进程是计算机中的一个独立的程序关于某数据集合的一次运行活动,是系统进行资源分配和调度的基本单位。每个进程都有自己独立的内存空间和系统资源,互不干扰。

进程一般由程序、数据集、进程控制块三部分组成。

  • 程序: 指进程所要执行的指令集合,包括可执行程序的机器语言代码和数据。
  • 数据集: 指进程所需要的数据,包括全局变量和局部变量。
  • 进程控制块: 是系统为每个进程维护的数据结构,记录了进程的当前状态,进程的基本信息,如进程ID,优先级,状态,进程的资源信息等。进程控制块是系统维护进程信息的重要数据结构,用于调度和管理进程,如记录进程。

最后,进程的局限是创建、撤销和切换的开销比较大。

  • 线程:

线程是进程的一个实体,是被系统独立调度和分派的基本单位,线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源 线程线程是在进程之后发展出来的概念。

线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源。

  • 协程:

协程: 协程是一种用户态的轻量级线程, 它是程序员可控制的(用户态执行),可以自行暂停和恢复执行,不由系统调度。

子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

然后我们站在协程的角度看看它有什么优缺点:

  • 优点:
  1. 协程是轻量级的,它没有线程那么大的系统开销,所以它比线程更容易创建和管理。
  2. 协程是可控的,程序员可以自行控制协程的暂停和恢复,这样可以更灵活的实现并发。
  3. 协程能够在单一线程中完成多任务的调度,这样可以减少线程上下文切换的开销。
  • 缺点:
  1. 协程需要额外的机制来避免数据冲突,这可能会增加程序的复杂性。
  2. 协程不能利用多核处理器的优势,因为它们运行在单一线程中。

go里面的协程是共享数据的。但是,Go语言提供了一些机制来避免在多个协程之间共享变量时的数据冲突。

Go语言提供了一种叫做channel的机制,允许协程之间进行通信。通过使用channel,可以在多个协程之间传递数据而不会发生数据冲突。

Go语言还提供了一种叫做互斥锁(mutex)的机制,用于在多个协程之间同步访问共享变量。使用互斥锁可以保证在某一时刻只有一个协程能够访问共享变量。

Goroutines

在Go语言中,使用关键字go来启动一个协程,如:

go foo()

这样就会在单独的协程中启动函数foo()。

协程之间可以使用通道(channel)来进行通信。通道是Go语言中的一种数据结构,可以用来在不同协程之间传递数据。

我们举一个具体的例子:

一个实用的 Go 协程案例是网络爬虫。网络爬虫程序通常需要同时访问多个网站,并在获取数据后进行处理。使用协程可以在访问一个网站时同时访问其他网站,提高爬取效率。例如:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.org",
    }

    for _, url := range urls {
        go fetch(url)
    }
    fmt.Scanln()
}

func fetch(url string) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Println(err)
        return
    }
    defer resp.Body.Close()
    fmt.Println(url, resp.Status)
}

在这个例子中,我们使用了 Go 内置的 net/http 包来访问网站,并在循环中使用 go 关键字来并发地执行 fetch 函数。这样,程序可以同时访问多个网站,而不会阻塞在一个网站上。

Channels

Channels 是 Go 语言中的一种通信机制,用于在 goroutines 之间进行同步和通信。通过 Channels,一个 goroutine 可以将数据发送到另一个 goroutine,并等待其接收。

Channels 类似于其他语言中的管道或队列,但是 Channels 在 Go 中是一种内置类型,并提供了丰富的操作方法。

下面是一个网络爬虫程序的例子,它使用了 Channels 来实现并发爬取,并在爬取完成后将数据发送到另一个 goroutine 进行处理:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    urls := []string{
        "http://www.example.com",
        "http://www.example.net",
        "http://www.example.org",
    }

    // 创建一个用于爬取的 channel
    fetchChannel := make(chan string)

    // 创建一个用于处理数据的 channel
    processChannel := make(chan string)

    // 启动多个 goroutine 进行爬取
    for _, url := range urls {
        go fetch(url, fetchChannel)
    }

    // 启动一个 goroutine 来处理数据
    go process(processChannel)

    // 从 fetch channel 中读取数据,并发送到 process channel
    for i := 0; i < len(urls); i++ {
        fetchResult := <-fetchChannel
        processChannel <- fetchResult
    }

    close(processChannel)
    fmt.Scanln()
}

func fetch(url string, fetchChannel chan string) {
    resp, err := http.Get(url)
    if err != nil {
        fetchChannel <- err.Error()
        return
    }
    defer resp.Body.Close()
    fetchChannel <- url + " " + resp.Status
}

func process(processChannel chan string) {
    for data := range processChannel {
        fmt.Println(data)
    }
}

在上面的例子中,我们使用了两个 channel

Go 语言中的包(package)是一种模块化编程的方式,用于将相关的类型、变量、函数和常量组织在一起。

所有的 Go 程序都必须在一个包中,main 包是一个特殊的包,它是程序的入口。

包中的类型、变量、函数和常量可以通过 import 关键字导入到其他包中使用。

例如:

在一个名为 math 的包中定义了一个名为 Add 的函数,它接受两个整型参数并返回它们的和。

package math

func Add(a, b int) int {
    return a + b
}

在另一个名为 main 的包中,我们可以导入 math 包并使用它的 Add 函数

package main

import "math"

func main() {
    result := math.Add(1, 2)
    fmt.Println(result)
}

执行这个程序将会输出 3。

Go 还支持匿名导入,可以使用 _ 关键字导入一个包,但不使用它的任何类型、变量、函数和常量。这可以用于导入一个包中的 init 函数。

例如:

import _ "math/rand"

这样会导入 math/rand 包,但不会使用任何类型、变量、函数和常量。

导入路径

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。

import (
    "fmt"
    "math/rand"
    "encoding/json"
    "golang.org/x/net/html"
    "github.com/go-sql-driver/mysql"
)

如果你计划分享或发布包,那么导入路径最好是全球唯一的。为了避免冲突,所有非标准库包的导入路径建议以所在组织的互联网域名为前缀;而且这样也有利于包的检索。例如,上面的import语句导入了Go团队维护的HTML解析器和一个流行的第三方维护的MySQL驱动。

包声明

在每个Go语音源文件的开头都必须有包声明语句。包声明语句的主要目的是确定当前包被其它包导入时默认的标识符(也称为包名)。

例如,math/rand包的每个源文件的开头都包含package rand包声明语句,所以当你导入这个包,你就可以用rand.Int、rand.Float64类似的方式访问包的成员。

package main

import (
    "fmt"
    "math/rand"
)

func main() {
    fmt.Println(rand.Int())
}

通常来说,默认的包名就是包导入路径名的最后一段,因此即使两个包的导入路径不同,它们依然可能有一个相同的包名。例如,math/rand包和crypto/rand包的包名都是rand。稍后我们将看到如何同时导入两个有相同包名的包。

关于默认包名一般采用导入路径名的最后一段的约定也有三种例外情况。

  • 第一种,包对应一个可执行程序,也就是main包,这时候main包本身的导入路径是无关紧要的。名字为main的包是给 go build 构建命令一个信息,这个包编译完之后必须调用连接器生成一个可执行程序。

  • 第二种,包所在的目录中可能有一些文件名是以*test.go为后缀的Go源文件。并且这些源文件声明的包名也是以_test为后缀名的。这种目录可以包含两种包:

    • 一种普通包,
    • 一种则是测试的外部扩展包。

    所有以_test为后缀包名的测试外部扩展包都由go test命令独立编译,普通包和测试的外部扩展包是相互独立的。后面会介绍test的内容

  • 第三种,一些依赖版本号的管理工具会在导入路径后追加版本号信息,例如"gopkg.in/yaml.v2"。这种情况下包的名字并不包含版本号后缀,而是yaml。"

测试

Maurice Wilkes,第一个存储程序计算机EDSAC的设计者,1949年他在实验室爬楼梯时有一个顿悟。在《计算机先驱回忆录》(Memoirs of a Computer Pioneer)里,他回忆到:“忽然间有一种醍醐灌顶的感觉,我整个后半生的美好时光都将在寻找程序BUG中度过了”。肯定从那之后的大部分正常的码农都会同情Wilkes过份悲观的想法,虽然也许不是没有人困惑于他对软件开发的难度的天真看法。

现在的程序已经远比Wilkes时代的更大也更复杂,也有许多技术可以让软件的复杂性可得到控制。其中有两种技术在实践中证明是比较有效的。第一种是代码在被正式部署前需要进行代码评审。第二种则是测试,也就是本章的讨论主题。

测试函数

每个测试函数必须导入testing包。测试函数有如下的签名:

func TestName(t *testing.T) {

// ...

}

测试函数的名字必须以Test开头,可选的后缀名必须以大写字母开头:

func TestSin(t *testing.T) { /* ... */ }

func TestCos(t *testing.T) { /* ... */ }

func TestLog(t *testing.T) { /* ... */ }

其中t参数用于报告测试失败和附加的日志信息。

我们来举一个例子:

gopl.io/ch11/word1

// Package word provides utilities for word games.
package word

// IsPalindrome reports whether s reads the same forward and backward.
// (Our first attempt.)
func IsPalindrome(s string) bool {
    for i := range s {
        if s[i] != s[len(s)-1-i] {
            return false
        }
    }
    return true
}

在相同的目录下,word_test.go测试文件中包含了TestPalindrome和TestNonPalindrome两个测试函数。每一个都是测试IsPalindrome是否给出正确的结果,并使用t.Error报告失败信息:

package word

import "testing"

func TestPalindrome(t *testing.T) {
    if !IsPalindrome("detartrated") {
        t.Error(`IsPalindrome("detartrated") = false`)
    }
    if !IsPalindrome("kayak") {
        t.Error(`IsPalindrome("kayak") = false`)
    }
}

func TestNonPalindrome(t *testing.T) {
    if IsPalindrome("palindrome") {
        t.Error(`IsPalindrome("palindrome") = true`)
    }
}

go test命令如果没有参数指定包那么将默认采用当前目录对应的包(和go build命令一样)。我们可以用下面的命令构建和运行测试。

$ cd $GOPATH/src/gopl.io/ch11/word1

$ go test

ok gopl.io/ch11/word1 0.008s
  • 测试用例名称一般命名为 Test 加上待测试的方法名。
  • 测试用的参数有且只有一个,在这里是 t *testing.T
  • 基准测试(benchmark)的参数是 *testing.B,TestMain 的参数是 *testing.M 类型。

go test -v-v 参数会显示每个用例的测试结果,另外 -cover 参数可以查看覆盖率

例如下面的:

$ go test -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
=== RUN   TestMul
--- PASS: TestMul (0.00s)
PASS
ok      example 0.007s

如果只想运行其中的一个用例,例如 TestAdd,可以用 -run 参数指定,该参数支持通配符 *,和部分正则表达式,例如 ^$

$ go test -run TestAdd -v
=== RUN   TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok      example 0.007s

子测试(Subtests)

子测试是 Go 语言内置支持的,可以在某个测试用例中,根据测试场景使用 t.Run创建不同的子测试用例:

// calc_test.go

func TestMul(t *testing.T) {
	t.Run("pos", func(t *testing.T) {
		if Mul(2, 3) != 6 {
			t.Fatal("fail")
		}

	})
	t.Run("neg", func(t *testing.T) {
		if Mul(2, -3) != -6 {
			t.Fatal("fail")
		}
	})
}
  • 之前的例子测试失败时使用 t.Error/t.Errorf,这个例子中使用 t.Fatal/t.Fatalf,区别在于前者遇错不停,还会继续执行其他的测试用例,后者遇错即停。

反射

反射是指在运行时动态获取和操作类型、变量、函数和接口的能力。在 Go 语言中,反射是通过内置的 reflect 包实现的。

常用的api如下:

reflect.ValueOf(x) 会返回一个 reflect.Value 类型的变量,它包含了变量的值和类型信息。通过这个变量,我们可以获取变量的值,修改变量的值,获取变量的类型和类别等。

reflect.TypeOf(x) 会返回一个 reflect.Type 类型的变量,它包含了变量的类型信息。通过这个变量,我们可以获取变量的类型名称,获取字段和方法等。

举个例子:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    var x float64 = 3.4
    v := reflect.ValueOf(x)
    fmt.Println("type:", v.Type())
    fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
    fmt.Println("value:", v.Float())
}

这里我们使用反射获取了一个 float64 类型的变量的类型、类别和值。 输出:

type: float64
kind is float64: true
value: 3.4

我们可以在运行时动态获取变量的类型,类别和值,并进行各种操作。

在 Go 中,反射还可以用来获取结构体的字段、方法和标签。

举个例子:

package main

import (
    "fmt"
    "reflect"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func (p Person) SayHello() {
    fmt.Println("Hello, my name is", p.Name)
}

func main() {
    p := Person{Name: "John", Age: 30}
    t := reflect.TypeOf(p)

    // 获取字段
    fmt.Println("fields:")
    for i := 0; i < t.NumField(); i++ {
        f := t.Field(i)
        fmt.Printf("%d: %s %s json=%s\n", i, f.Name, f.Type, f.Tag.Get("json"))
    }

    // 获取方法
    fmt.Println("\nmethods:")
    for i := 0; i < t.NumMethod(); i++ {
        m := t.Method(i)
        fmt.Printf("%d: %s\n", i, m.Name)
    }
}

输出:

fields:
0: Name string json=name
1: Age int json=age

methods:
0: SayHello

我们通过反射获取了结构体 Person 的字段名称、类型和标签,以及方法名称。

最基础的部分已经完结

接下来,是看了一个视频(<Go语言从入门到实战>),总结了一些语法上的常见坑点。

类型转化

与ts的区别:

1、Go语言不允许隐式类型转换 2、别名和原有类型也不能进行隐式类型转换

package main

func main(){
    var a intt = 1
    var b int64
    b = a // 报错
}

如何修正,显示类型转换即可

 b = int64(a)

我们再看下别名

package main

type MyInt int64

func main(){
    var b int64
    var c MyInt
    c = b // 报错 修正的话:c = MyInt(b)
}

指针类型

  • 不支持指针运算
  • 与ts的区别: string是值类型,默认的初始化值是空字符串,不是undefined
package main

type MyInt int64

func main(){
    a := 1
    aPoint := &a
    aPoint := aPoint + 1
    var b string // 被初始化为一个空值
}

算数运算符

  • go没有前置++

while循环

与js的差别,没有while循环,但可以用for循环来实现

n := 0
for n < 5 {
    n++
    fmt.Println(n)
}

if语句

怎么说呢,if的go风格是下面这样的,跟js不太像,跟node其实有点像,node以前是回调函数都有错误优先,go也是这样的,只不过写起来语法不一样,思想是一样的。

package main


func main(){
   if v, err := someFun(); err == nil {
       xxx
   }  else {
       xxx
   }
}

switch语句也有类似的用法

switch os:= runtime.GOOS; os {
    case "darwin":
        fmt.Println("OS X.")
    case "linux"
        fmt.Println("Linux.")
    default:
        fmt.Printf("%s.", os)
}

switch还有一种用法,跟js完全不同,下面的逗号相当于匹配任意一个就算true

switch i {
    case 0, 2:
        xx
    case 1, 3:
        xx
    default:
        xx
}

多维数组

例如如何在go中声明2维数组

c := [2][2]int{{1, 2}, {3, 4}}

go中判断map中某个key是否元素存在

写法如下,跟js大不相同

m1 := map[int]int{}
m1[2] = 0

if v,ok := m1[3]; ok {

} else {

}

go中的slice在什么情况下会共享数据

在 Go 中,slice 是对数组的一个封装。当一个新的 slice 是由一个已经存在的 slice 创建时,两个 slice 会共享同一个底层数组。

这可能会发生在以下两种情况:

  1. 通过切片语法创建新的 slice:
original := []int{1, 2, 3, 4, 5}
// Create a new slice that shares the same underlying array
new := original[1:3]
  1. 通过调用内置函数 make 创建新的 slice,并且指定了第三个参数,并且这个参数不为0,表示容量而不是长度,这样创建的slice 会共享同一个底层数组:
original := make([]int, 5, 10)
// Create a new slice that shares the same underlying array
new := original[:3]

在这些情况下,原始的 slice 和新的 slice 都共享相同的底层数组。当修改其中一个 slice 中的数据时,另一个 slice 中的数据也会被更改。

当然在创建新的slice的时候如果是通过append()来创建的就不会共享数据了,因为append会新开一块新的内存来存储数据。

go里面的相面对象其实没有严格的继承功能

网上很多文章说go支持继承,其实并不支持,严格来讲应该算是组合,而不是继承,类似设计模式的桥接模式。

在Go语言中,可以通过组合一个类型的结构体字段来实现类似继承的功能。这种方式更加简洁、清晰,同时也避免了继承所带来的复杂性和灵活性问题。

go中的字符串表现为Unicode字符组成的byte切片

在 Go 语言中,字符串是由一系列Unicode字符组成的,字符串在内存中是以UTF-8编码的形式存储的。实际上,字符串在内存中是一个byte切片,每个字符在内存里是一段连续的区间。

空接口可以表示任何类型

在 Go 中,空接口(interface{})表示可以存储任何类型的值。因为所有类型都满足空接口的约束(不需要实现任何方法),所以可以将任何类型的值赋值给空接口类型的变量。这使得空接口非常适用于实现通用函数、数据结构等。

注意,可以通过断言来将空接口转换为指定类型

v, ok := p.(int) // ok等于true时,转换成功

go接口的最佳实践

倾向于使用小的接口定义,很多接口只包含一个方法,比如

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

较大的接口定义,可以由多个小接口定义组合而成

type ReadWriter interface {
    Reader
    Writer
}

只依赖于必要功能的最小接口

func StoreData(reader Reader) error { ... }

GO没有错误机制

比如没有try catch语句。我们可以通过errors.New来快速创建错误实例,利用多返回值的特性,去处理错误

errors.New("n must be int rhe range [0, 1]")

错误处理原则

及早失败,避免嵌套

就是说如果函数报错,就直接return了

错误的recover

常常有同学这样写代码:

defer func() {
    if err := recover(); err != nil {
        log.Error("recover panic", err)
    }
}

因为错误可能是我们系统中某些资源消耗完了,我这样恢复了,其实系统依然是不能工作的,我们的健康检查也很难去检查出现在的问题,因为常常健康检查只是检测当前的应用是否正常提供服务,结果还是在的,结果你已经不能提供正常的服务了。

如何处理呢?

"Let it Crash",重启大法好啊!

package一致

同一目录里的Go代码的package要保持一致,要不编译不通过

init可以定义多个

首先 init函数时在main函数被执行前,所有依赖的package的init方法都会被执行

并且包的每个源文件也可以有多个init函数,这点比较特殊

例如:

// my_series.go

package series

func init() {
    fmt.Println("init1")
}

func init() {
    fmt.Println("init2")
}

func Square(n int) int {
    return n * n
}
package main

import "ch15/series"

func main() {
     fmt.Println(series.Square(2)) 
}

最后输出,init的函数依次执行了

go get 命令默认访问https

"go get -u" 是 Go 语言中的命令,用于安装和更新 Go 包。

"go get" 命令用于安装 Go 包,它会从远程代码库下载包的源代码,并安装到本地。

"-u" 参数表示更新,当安装已经存在的包时,会更新这个包到最新版本。

例如,执行 "go get -u github.com/golang/example" 命令,会安装或更新名为 "example" 的包,并将其安装到 $GOPATH/src/github.com/golang 目录下。

需要注意的是,go get命令默认会访问https的代码库,如果你需要访问http的代码库或者本地目录,需要使用-d参数。

协程的原理

go的协程开销非常小,初始化的栈只有2k,而java的线程栈大小是1M。java线程和内核对象的映射是1:1,而go的协程和内核对象的映射是M:N(多对多),明显go要更强。

协程并发处理的机制:

协程.png

上图中,M代表的是系统线程,P是GO语言自己实现的协程处理器,G是一个协程任务,组成协程队列。

如果一个协程花费的时间特别长

机制1:协程的守护进程会记录协程处理器完成的协程数量,如果一段时间内,处理器完成的数量没有变化,就会往任务栈中插入一个标记,当前协程遇到非内联函数时,在任务栈中读到该标记,就会把该协程任务移到队列的末尾;

机制2:如果执行协程过程中遇到一个非CPU密集型任务,例如IO,需要中断等待时,协程处理器P会把自己移到另外一个可使用的系统线程中,继续执行队列中其他的协程任务。当中断的协程被重新唤醒后,会把自己重新加入到协程队列里或者全局等待队列中。其中,中断的协程会把context保存在协程对象里,当协程被唤醒后会将其重新读取到寄存器中,继续运行。

一个很容出错的案例:

func TestGroutine(t *testing.T) {
    for i := 0; i < 10; i++ {
        go func(i int) {
            fmt.Println(i)
        }()
    }
}

其中i打印的值都是10,因为go 后面的代码有点像js的异步,for循环是同步代码,当循环完毕,i已经是10了,然后因为go又是词法作用域,所以,每次去寻找外部i的值,都是10。

参考: