GO入门(微软教程笔记)

原教程地址:微软Go入门

安装

此处不赘述,官网走起(可能需要kx上网)。


语法:

变量

声明变量:

var(变量名 类型 [= 数值]) []为可选项
var 变量名 类型
var 变量名 = xxxx
变量名 := xxx

注意,每个变量声明之后,必须有地方使用到,不然就无法run。


Go 是一种强类型语言。 这意味着你声明的每个变量都绑定到特定的数据类型,并且只接受与此类型匹配的值。(VsCode搭配官方的工具,每次ctrl+s之后,代码都会被格式化。同时,可以看到提示的各种语法问题,编译问题等等。)

Go 有四类数据类型:
基本类型:数字(int32 int64 int...)、字符串(string)和布尔值(bool)
聚合类型:数组和结构(struct)
引用类型:指针(& *)、切片(slice)、映射、函数(func)和通道(channel)
接口类型:接口(interface)

rune只是为了区分int和char,源码如下:

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32

有时,你需要对字符进行转义。 为此,在 Go 中,请在字符之前使用反斜杠 ()。 例如,下面是使用转义字符的最常见示例:
\n:新行
\r:回车符
\t:选项卡
':单引号
":双引号
\:反斜杠

iota用于自增的常量使用,效果更佳,示例如下:

type ByteSize float64

const (
    _           = iota // ignore first value by assigning to blank identifier
    KB ByteSize = 1 << (10 * iota)
    MB
    GB
    TB
    PB
    EB
    ZB
    YB
)

挑战:可以尝试用iota赋值一周的七天。

weekdays.png


函数(func):
语法:

func name(parameters) (results) {
    body-content
}
  • os.Args 变量包含传递给程序的每个命令行参数。
  • Go 是“按值传递”编程语言。 这意味着每次向函数传递值时,Go 都会使用该值并创建本地副本(内存中的新变量)。

在 Go 中,有两个运算符可用于处理指针:

  • & 运算符使用其后对象的地址。
    • 运算符取消引用指针。 也就是说,你可以前往指针中包含的地址访问其中的对象。

模块 & 库...

语法:

  • 初始化模块: go mod init 模块名 ;
  • 升级模块: go get 外部模块名@版本号(其中,“@版本号”可不填,默认为@latest,即为最新版本) ;
  • 列出当前模块的所有依赖: go list -m all
  • 列出模块可用版本: go list -m -versions 外部模块名
  • 多个版本存在,或者有的库已经用不上了: go mod tidy 可以去除相应的require行

举个栗子:
go mod init example.com/hello, 会获得一个go.mod文件:

module example.com/hello

go 1.15

当import 库之后,go.mod会增加require选项,比如:

import (
    "rsc.io/quote"
    quoteV1 "rsc.io/quote/v3"
)

查看go.mod:

module example.com/hello

go 1.15

require (
    golang.org/x/text v0.3.5 // indirect
    rsc.io/quote v1.5.2
    rsc.io/quote/v3 v3.1.0
    rsc.io/sampler v1.3.1 // indirect
)

同时,会出现go.sum,这部分是为了确保项目所依赖的模块不会由于恶意,意外或其他原因而意外被更改。(个人理解为对文件做哈希值校验。)

关于modules的使用,参考官方博客
关于语义导入版本控制(Semantic Import Versioning),参考此处


defer/panic/recover

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

package main

import (
    "io"
    "os"
)

func main() {
    f, err := os.Create("notes.txt")
    if err != nil {
        return
    }
    defer f.Close()

    if _, err = io.WriteString(f, "Learning Go!"); err != nil {
        return
    }

    f.Sync()
}

panic(内置 panic() 函数会停止正常的控制流。 所有推迟的函数调用都会正常运行。 进程会在堆栈中继续,直到所有函数都返回。 然后,程序会崩溃并记录日志消息。 此消息包含错误和堆栈跟踪,有助于诊断问题的根本原因。)

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)
}

recover(Go 提供内置函数 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)
}

数组、切片、映射、结构

数组(定长)的例子:

q := [...]int{1, 2, 3} // 一维省略号可以帮助计算出数组的长度。
var twoD [3][5]int // 二维数组
var threeD [3][5][2]int // 二维数组
for i := 0; i < 3; i++ {
        for j := 0; j < 5; j++ {
            for k := 0; k < 2; k++ {
                threeD[i][j][k] = (i + 1) * (j + 1) * (k + 1)
            }
        }
}
fmt.Println("\nAll at once:", threeD)

切片只是名为基础数组的数组之上的一种数据结构。(数据底层还是一个数组。)通过切片,可访问整个基础数组,也可仅访问部分元素。切片只有 3 个组件:

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

例子:

package main

import "fmt"

func main() {
    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]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}
切片核心

Go 具有内置函数copy(dst, src []Type) 用于创建切片的副本。make([]Type, 3)用来创建切片。(个人理解是:用这种方式来填补深拷贝和浅拷贝的差异。)

package main

import "fmt"

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

    slice1 := letters[0:2]
    slice2 := letters[1:4]
    slice3 := make([]string, 3)
    copy(slice3, letters[1:4])

    slice1[1] = "Z"

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

映射(map):映射是动态的。 创建项后,可添加、访问或删除这些项。(无他,就是键值对。)

// 方式1(直接给数值)
studentsAge := map[string]int{
        "john": 32,
        "bob":  31,
}
// 方式2(make初始化)
studentsAge := make(map[string]int)
studentsAge["john"] = 32
studentsAge["bob"] = 31

// 访问映射中没有的项时 Go 不会返回错误,这是正常的。 但有时需要知道某个项是否存在。
// 在 Go 中,映射的下标表示法可生成两个值。 第一个是项的值。 第二个是指示键是否存在的布尔型标志。
val, exist := studentsAge["christy"]
fmt.Println("Christy's age is", studentsAge["christy"])

// 若要从映射中删除项,请使用内置函数 delete()。
delete(studentsAge, "john")

// 可使用基于范围的循环
for name, age := range studentsAge {
    fmt.Printf("%s\t%d\n", name, age)
}

// 如果对string进行循环时,每个都是rune类型,但是如果取下标的话,则是byte

结构,语法及例子如下:

type 结构名 struct {
  字段 类型 
  字段 类型
}
// (go是传值的语言,要修改还是得用指针类型。)
package main

import "fmt"

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    fmt.Println(employee)
    employeeCopy := &employee // 不用指针改不了值
    employeeCopy.FirstName = "David"
    fmt.Println(employee)
}
// 结构嵌套
package main

import "fmt"

type Person struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

type Employee struct {
    Person
    ManagerID int
}

type Contractor struct {
    Person // 可以不给字段名,但是初始化的时候要给清楚该字段的值
    CompanyID int
}

func main() {
    employee := Employee{ // 给清楚Person类型对应的属性
        Person: Person{
            FirstName: "John",
        },
    }
    employee.LastName = "Doe"
    fmt.Println(employee.FirstName)
}

处理Json:json.Marshal以及json.Unmarshal

package main

import (
    "encoding/json"
    "fmt"
)

type Person struct {
    ID        int
    FirstName string `json:"name"`
    LastName  string
    Address   string `json:"address,omitempty"` // omitempty忽略空值,减少传输开销。
}

type Employee struct {
    Person
    ManagerID int
}

type Contractor struct {
    Person
    CompanyID int
}

func main() {
    employees := []Employee{
        Employee{
            Person: Person{
                LastName: "Doe", FirstName: "John",
            },
        },
        Employee{
            Person: Person{
                LastName: "Campbell", FirstName: "David",
            },
        },
    }

    data, _ := json.Marshal(employees) // 由于存在omitempty选项,json里没有address的内容,减少传输开销。
    fmt.Printf("%s\n", data)

    var decoded []Employee
    json.Unmarshal(data, &decoded)
    fmt.Printf("%v", decoded)
}

错误&&日志

通用做法如下,把可能的错误返回出来:

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}
  • 始终检查是否存在错误,即使预期不存在。 然后正确处理它们,以免向最终用户公开不必要的信息。(比如以前写后台的时候,数据库错误之类的,在测试的时候,被打印出来,影响到用户体验。)
  • 在错误消息中包含一个前缀,以便了解错误的来源。 例如,可以包含包和函数的名称。在记录错误时记录尽可能多的详细信息,并打印出最终用户能够理解的错误。(提供更多debug的(给人看的)信息。)
  • 创建尽可能多的可重用错误变量。(重用更多的错误类型,比如404之类的错误。)
  • 了解使用返回错误和 panic 之间的差异。 不能执行其他操作时再使用 panic。 例如,如果某个依赖项未准备就绪,则程序运行无意义(除非你想要运行默认行为)。(尽量程序处理好各种错误,不要panic再来recover。)

记录日志(log库)、存入文件(os库):

package main

import (
    "log"
    "os"
)

func main() {
    file, err := os.OpenFile("info.log", os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        log.Fatal(err)
    }

    defer file.Close()

    log.SetOutput(file)
    log.Print("Hey, I'm a log!")
}

Go 的几个记录框架有 Logrus、zerolog、zap 和 Apex。

以zerolog为例,安装:
go get -u github.com/rs/zerolog/log

package main

import (
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"
)

func main() {
    zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
    log.Print("Hey! I'm a log message!")
}

主要是为了管理日志级别比较方便,打印如下:{"level":"debug","time":1609855453,"message":"Hey! I'm a log message!"}


接口

以下创建图形接口,然后实现正方形的结构体,实现接口中的方法,就是等于实现了接口。那么,可以用图形结构体初始化一个正方形(因为已经实现了该接口,调用的是被实现的方法。)

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

type Square struct {
    size float64
}

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

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

func main() {
    var s Shape = Square{3}
    fmt.Printf("%T\n", s)
    fmt.Println("Area: ", s.Area())
    fmt.Println("Perimeter:", s.Perimeter())
}

实现net/http 程序包中的 http.Handler 接口:

package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

// 就可以把实现的具体结构作为参数传递给ListenAndServe()的第二个参数了。
type database map[string]dollars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    for item, price := range db {
        fmt.Fprintf(w, "%s: %s\n", item, price)
    }
}

func main() {
    db := database{"Go T-Shirt": 25, "Go Jacket": 55}
    log.Fatal(http.ListenAndServe("localhost:8000", db))
}

并发

需要注意:个人理解,主程序为主进程,如果在子线程执行函数的过程中,主进程的程序已经运行完了。此时,程序会直接退出,而不会等待子线程完成指令。

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

关于Channel,官方博客值得一看:Share Memory By Communicating。意思是通过通信来共享内存。
无缓冲channel(个人理解为同步的意思,有线程放进变量,必得有线程取出变量,不然就会出现Dead Lock的报错提醒。)

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

有缓冲channel

func send(ch chan string, message string) {
    ch <- message
}

func main() {
    size := 2
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    go send(ch, "three")
    go send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < 4; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

select关键字是switch的channel版本。
如何在使用 select 关键字的同时与多个 channel 交互的简短主题。 有时,在使用多个 channel 时,需要等待事件发生。

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)
        }
    }
}

测试

  • 如何在 Go 中进行测试。(通过使用测试驱动开发)。

编写代码时要遵循的一个良好做法是使用测试驱动开发 (TDD) 方法。 使用此方法时,我们将首先编写测试。 我们可以肯定那些测试会失败,因为它们测试的代码还不存在。 然后,我们将编写满足测试条件的代码。

创建测试文件时,该文件的名称必须以 _test.go 结尾。 要编写的每个测试都必须是以 Test 开头的函数。 然后,你通常为你编写的测试编写一个描述性名称,例如 TestDeposit。

举例:

package bank

import "testing"

func TestAccount(t *testing.T) {

}

输出如下:

=== RUN   TestAccount
--- PASS: TestAccount (0.00s)
PASS
ok      github.com/msft/bank    0.391s

测试命令:
go test -v


(To be continue) 后续熟练Go,上手CRUD,尝试做个Web项目。
推荐:
effective go:如何在Go领域下,写出自己的高效。
go web wiki:写基础Web应用。

你可能感兴趣的:(GO入门(微软教程笔记))