学习Go笔记

154 阅读14分钟

前言

Go是一门很像解释型语言的编译型语言。一方面它的性能远远超越于js,php等解释型语言,接近于C;另外一方面又没有C/C++语言编译极耗时的缺点;它在开发体验和性能之间取得了一个很好的平衡点。另外,Go语言语法简洁干净,自带垃圾回收,对高并发和充分利用多核机器的支持非常友好。

类型系统

go是编译型语言,build之前有编译的环节,编译的过程不仅会进行语法检测,还会进行类型检测,这能提前发现绝大部分问题。go是强类型语言,所以类型不正确是无法通过编译。

基础类型

分类说明
布尔类型bool
无符号整型uint uint8/byte uint16 uint32 uint64 uintptr
有符号整型int int8 int16 int32/rune int64
浮点型float32 float64
复数类型complex64 complex128
  • byte是uint8的别名,rune是int32的别名,表示一个unicode字符。
  • uint、int和uintptr 在32位系统长度为32位,在64位系统长度为64位。
  • uintptr用来存储内存地址,go不识别内存地址存储的内容,所以慎重使用。
  • 复数类型一般用于数学运算,complex64 实部和虚部都是32位
package main

var num int = 666

func main() {
    // 函数内可以用 := 语法,直接声明赋值,类型也会自动推断
    my_num := 777
    // 复数
    var val complex64 = 32 + 32i
    // 默认是128位,虚部和实部都是64位
    value28:= 32 + 32i
    
    // const 用来定义常量
    const PI = 3.14159

}

复杂类型

对于基础类型之外的原生内置类型可以归类为复杂类型,主要有指针、数组/切片、映射map、结构体struct。

指针

与C语言类似,指针类型变量保存着指向对应变量的地址,通过这个地址可以拿到变量,通过变量也可以获得地址。与C语言不同的是,go的指针不能进行指针运算,即不能进行指针+1指向下个地址的运算。

package go

func main() {
    val := 888
    valPtr := &val
    // 与上面的赋值方式等价
    var valPtr2 *int = &val
    fmt.Println("valPtr == valPtr2:", valPtr == valPtr2) // true
    fmt.Printf("valuePtr: %T\n", valPtr)                 // *int
    // 通过指针更新值
    *valPtr2 = 666
    fmt.Println("value after update:", val, *valPtr) // 666  666
}

数组/切片

在go中,数组类型是长度固定的,长度也是其类型的一部分,[5]int[6]int 是两个不同的类型。当需要可变长度的数组时,可以使用切片的方式。

  • 通过数组生成的切片,底层引用的还是数组,修改切片相当于修改数组。
  • 切片会自动扩容,扩容时是拷贝了一份,底层内存地址与扩容前不一致。
  • 可以使用make 函数来创建切片,创建时可以指定容量。

package main

func  main() {
    // array
    var arr = [4]int{1, 2, 3, 4}
    fmt.Printf("len(arr)=%d;cap(arr)=%d\n", len(arr), cap(arr))
    // slice
    // 从数组中切片,底层引用的还是数组
    slice := arr[1:3]
    fmt.Println("slice:", slice)                                        // [2 3]
    fmt.Printf("len(slice)=%d;cap(slice)=%d\n", len(slice), cap(slice)) // 2, 3
    slice = append(slice, 5)
    fmt.Println(arr, slice) // [1 2 3 5] [2 3 5]
    slice = append(slice, 6)
    slice[1] = 0
    fmt.Println(arr, slice) // [1 2 3 5] [2 0 5 6]

    // 创建长度为0,容量为0的切片, 长度不够时切片会自动扩展
    slice2 := make([]string, 0)
    fmt.Printf("len(slice)=%d;cap(slice)=%d\n", len(slice2), cap(slice2))
    slice2 = append(slice2, "hello")
    fmt.Printf("len(slice)=%d;cap(slice)=%d\n", len(slice2), cap(slice2))
    slice2 = append(slice2, "world")
    fmt.Println(slice2, slice2[0], slice2[1]) //[hello world] hello world
}

map键值对

map的键必须是固定长度的。 在golang规范中,可比较的类型都可以作为map key,包括:

类型说明
boolean布尔值
numeric数字 包括整型、浮点型,以及复数
string字符串
pointer指针 两个指针类型相等,表示两指针指向同一个变量或者同为nil
channel通道 两个通道类型相等,表示两个通道是被相同的make调用创建的或者同为nil
interface接口 两个接口类型相等,表示两个接口类型 的动态类型 和 动态值都相等 或者 两接口类型 同为 nil
struct、array只包含以上类型元素

不能作为key的类型有:map,slice和func。

package main

func main() {
    // map
    m := map[string]int{
            "a": 12,
            "b": 0,
    }
    m["a"]++
    m["c"]++
    fmt.Println(m) // map[a:13 b:0 c:1]
    delete(m, "b")
    fmt.Println(m) // map[a:13 c:1]
    m1 := make(map[float32]int)
    fmt.Println(m1)
}

struct结构体

go允许定义结构体来组织数据。

package main 

type Persion struct {
        Name  string
        Age   int
        IsMan bool
}

func main() {
    p := Persion{"perry", 23, true}
    fmt.Println(p.Name, p.Age, p.IsMan)
    ptr := &p
    // go语法糖,会把ptr.Name 编译成(*ptr).Name
    fmt.Println(ptr.Name, ptr.Age, ptr.IsMan)
}

枚举

golang中是没有枚举类型,但是可以使用const实现枚举。需要注意的是,golang中没有typescript中的字面量类型,即在ts中某个值也可以是一种类型。

type HttpStatus string

const (
    NOT_FOUND    HttpStatus = "666"
    SERVER_ERROR HttpStatus = "123"
    REDIRECT     HttpStatus = "666"
)

func myStatus(status HttpStatus) {
    fmt.Println(status)
}

func TestGeneracity() {
    val := REDIRECT
    myStatus(val)

    val2 := "666"
    // 报错, val2是string类型,不是HttpStatus
    myStatus(val2)
}

type Week int

// iota表示自增
const (
    Monday    Week = iota // Monday = 0
    _                     // 1 被跳过
    Tuesday               // Tuesday = 2
    Wednesday             // Wednesday = 3
    Thursday              // Thursday = 4
    _                     // 5 被跳过
    Friday                // Friday = 6
    Saturday              // Saturday = 7
    Sunday                // Sunday = 8
)

函数

函数也是一种类型,运行定义函数变量作为参数或者返回值进行传递。

  • 函数允许有多个返回值,返回值也允许命名。
  • 函数允许作为变量定义,由此可以形成闭包(与js的闭包类似)。
  • 函数还可以作为某个类型的方法,任何类型都可以定义方法,但需要类型定义和方法定义在同一个package内,所以不能为原生类型定义方法的。
package main


func hello(name string) (string, error) {
    if name == "" {
            return "", errors.New("empty name")
    }

    return fmt.Sprintf("你好啊!%s\n", name), nil
}

type Persion struct {
	Name  string
	Age   int
	IsMan bool
}

// 作为Persion类型的方法
func (p Persion) printInfo() {
	fmt.Println(p.Name, p.Age, p.IsMan)
}


func main() {
    res,error:=hello("Perry")
    if error != nil {
        fmt.Println(error)
    } else  {
        fmt.Println(res)
    }
    
    var hello1 func() string = func() string {
            return "hello world"
    }
    fmt.Printf("hello type: %T\n", hello) // hello type: func() string
    
    p := Persion{"perry", 23, true}
    // 调用方法
    p.printInfo()
}

接口类型

接口类型定义了一组方法签名,凡是有与之相同签名的方法的类型都是其子类型。从某种程度来说,接口可以帮助我们绕过编译器的类型检测。

package main

import "fmt"


// 定义接口
type Print interface {
	Print()
}
type Persion struct {
	Name  string
	Age   int
	IsMan bool
}

func (p Persion) Print() {
	fmt.Println(p.Name, p.Age, p.IsMan)
}

type MyInt int

func (m MyInt) Print() {
	fmt.Println(m)
}

func doPrint(o Print) {
	o.Print()
}

// 任何类型都实现了0个方法,相当于关掉了类型检测
type any interface{}

func main() {
    p := Persion{"perry", 12, true}
    var mi MyInt = 12
    // Persion和MyInt都实现了Print方法,所以实现了接口
    doPrint(p)
    doPrint(mi)


    var a any = 888
    fmt.Println(a)
}


自定义类型

前面几节,我们定义结构体,接口都使用了type关键字,正是如此,type关键字用来定义新的类型。我们可以使用它结合内置类型定义出我们想要的类型。

type MFloat float32;

type IInt int8; 

func (ii IInt)Abs(): IInt {
    return -ii;
}

流程控制

任何一门图灵完备的语言都需要有流程控制语句,golang的确如此,而且golang的流程控制语句比起其它语言简化了许多。

条件分支

golang中条件分支有ifelseswitchif else与其它语言基本一致,switch则不然,switch不需要手动break,每个case都是自动break,但如果想实现穿透也还是可以的,可以使用fallthrough

package main

func main() {
    isRight := true

    if isRight {
            fmt.Println("is right.")
    } else {
            fmt.Println("is not right.")
    }

    val := "hello"

    switch val {
    case "abc":
            fmt.Println("is abc")
    case "world":
            fmt.Println("is world")
    default:
            fmt.Println("is other")
    }
    // 类似 if then else
    switch {
    case val == "abc":
            fmt.Println("is abc")
    case val == "world":
            fmt.Println("is world")
    default:
            fmt.Println("is other")

    }

   
   // 对于接口还可以判断它的类型
    var an interface{}
    switch an.(type) {
    case string:
            fmt.Println("is string")
    case int16:
            fmt.Println("is int16")
    case nil:
            fmt.Println("is nil")
    default:
            fmt.Println("do not know its type.")

    }

}

func main() {
    switch n := rand.Intn(100) % 5; n {
    case 0, 1, 2, 3, 4:
            fmt.Println("n =", n)
            fallthrough // 跳到下个代码块
    case 5, 6, 7, 8:
            // 一个新声明的n,它只在当前分支代码快内可见。
            n := 99
            fmt.Println("n =", n) // 99
            fallthrough
    default:
            // 下一行中的n和第一个分支中的n是同一个变量。
            // 它们均为switch表达式"n"。
            fmt.Println("n =", n)
    }
}
/**打印结果
n = 1
n = 99
n = 1

*/

循环

golang只有for一种循环, 但是使用for可以实现while的效果。

package main

import "fmt"

func main() {
    testFor()
}

func testFor() {
    for i := 0; i < 5; i++ {
        fmt.Println(i)
    }
    i := 0

    // 实现while的效果
    for i < 5 {
        fmt.Println(i)
        i++
    }

    var k = 0
    // 无限循环
    for {
        fmt.Println(k)
        k++

        if k > 10 {
            break
        }

    }

    arr := []string{
        "hello",
        "world",
        "!",
    }
    // range 类比 js Object.entries
    for index, val := range arr {
        fmt.Println(index, val)
    }

}

goto

Golang引入了goto关键字,用于跳转到指定代码执行。需要注意的是,只有在你使用其它逻辑语句无法实现,而且使用goto语句能代码更清晰时才应该使用gotogoto的滥用会使代码的可读性变得极差。

func main() {
    for {
            switch n := rand.Intn(100); n {
            case 0, 1, 2, 3, 4:
                    fmt.Println("n =", n)
            case 5, 6, 7, 8:
                    fmt.Println("n =", n) // 99
            case 9:
                    goto NEXT
            }
    }

NEXT:
    fmt.Println("next")
}

defer

与其它语言不太一样,golang还多了一个defer流程控制语句。defer允许在函数结束前做一些事情,这样可以在一些需要回收的场景的代码写得更内聚。一个函数内多个defer会把它们加到栈中,执行的时候后进先出。

func testDefer() {
    // 输出顺序为start -> center -> end-> defer-2 -> defer-1
    fmt.Println("start")
    defer (func() {
            fmt.Println("defer-1")
    })()
    fmt.Println("center")
    defer (func() {
            fmt.Println("defer-2")
    })()
    fmt.Println("end")
}

封装

什么是封装?

举个不恰当的例子,遥控无人机的控制非常复杂,按下向上按钮,遥控器要生成指令,加密,发送信号..无人机向上攀升,这个过程非常复杂。但对于使用者来说,他无需关心无人机内部是如何控制的,他只要知道按下up按钮就会向上,按下down按钮就会向下,所以无人机的设计者用了一个壳把遥控器包起来了,只对外暴露了几个按钮。

编程中封装也是如此的,代码有各种各样的逻辑,我们把各个逻辑都封装起来,用个“壳”把逻辑内部的变量和方法隐藏起来,只对外暴露外界需要感知的接口,调用方通过这些接口实现功能。经过封装的代码,相同的逻辑集中到一起,高内聚;与外界也只通过几个接口进行交互,低耦合。

在js,java等语言中都是通过类class来进行封装,而go是没有类的,那么它是怎么实现封装的呢? go是通过package来进行封装的,它武断地定义了几个限制:

  • 包内以大写开头的变量、类型或函数才会对外暴露。
  • 结构体内也是大写开头的属性才会对外暴露。
  • 包内顶层作用域只允许声明变量,不允许执行其它语句,如执行函数。
  • 所有代码都应该写在包内。

由此我们可以把golang的package类比成js的class,在package内定义的变量和函数就是class的属性和方法。从某种程度上说,golang强制所有代码都使用class(package)来组织。

多态

golang通过interface支持了多态。与java不同的是,在golang中,实现方不需要显式实现接口,只要具有接口所有方法签名就算实现接口,这样能够进一步解耦,但也会造成额外的维护成本。

type IHandle interface {
	Handle(command string)
}

type Human struct {
	Name string
	Age  int
}

func (h Human) Handle(command string) {
	fmt.Println("human:", h.Name, h.Age, command)
}

type Animal struct {
	T    string
	Name string
}

func (a Animal) Handle(command string) {
	fmt.Println(a.T, a.Name, command)
}

// h 是IHandle类型,接收所有实现了IHandle的类型
func doHandle(h IHandle, command string) {
	h.Handle(command)
}

func main() {
    h := Human{"perry", 28}
    an := Animal{"cat", "喵喵"}
    doHandle(h, "散步")
    doHandle(an, "喂猫")

}

泛型

泛型为类型提供了参数。为什么需要泛型? 这一切的源头在于强类型,强类型让我们在编译阶段就发现了大部分问题,也带来了限制:

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

func Add2(a float32, b float32) float32 {
    return a+b
}

以上两个函数,算法完全一致,仅仅是类型不一样,但是需要定义两个,泛型正是用来解决这样问题的。

func [T int|float32]Add(a , b T) T {
    return a +b
}

type Array[T comparable] []T
var stringArr Array[string]
var intArr Array[int]

多线程

golang通过go关键字对多线程提供了支持。

package generacity

import (
	"fmt"
	"time"
)

func say(val string) {
    for i := 0; i < 10; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(val, i)
    }
}

func TestGoroutine() {
    go say("world")
    say("hello")
}
/* 打印结果
hello 0
world 0
world 1
hello 1
world 2
hello 2
hello 3
world 3
world 4
hello 4
hello 5
world 5
hello 6
world 6
hello 7
world 7
world 8
hello 8
hello 9
world 9
*/

可以看到hello和world交叉打印了,说明有两个线程在交替执行。

chain信道

信道主要用于线程间的通信。

  • 默认情况下,在发送方或接收方没有准备好之前,该线程会阻塞,即在完成通信之后才能往下执行。
  • 可以给信道设置缓冲区,在缓冲区满之前不会阻塞。
func sum(arr []int, c chan int) {
    defer fmt.Println(arr)
    res := 0
    for _, val := range arr {
        res += val
    }
    c <- res

}

func TestGoroutine() {
    c := make(chan int)
    arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 23, 12, 34, 456, 23}
    go sum(arr[:len(arr)/2], c)
    go sum(arr[len(arr)/2:], c)
    time.Sleep(100 * time.Millisecond)
    a, b := <-c, <-c
    fmt.Println("all sum:", a+b)
    // 等待子线程打印,避免退出程序,看不到打印
    time.Sleep(100 * time.Millisecond)
    /**打印顺序 
    all sum: 602
    [1 2 3 4 5 6 7]
    [8 9 9 23 12 34 456 23]
    */
    // 设置长度为2的缓冲区
    c1 := make(chan int, 2)
    go sum(arr[:len(arr)/2], c1)
    go sum(arr[len(arr)/2:], c1)
    e, d := <-c1, <-c1
    fmt.Println("all sum2:", e+d)
    time.Sleep(100 * time.Millisecond)
    /**打印顺序
    [8 9 9 23 12 34 456 23]
    [1 2 3 4 5 6 7]
    all sum2: 602
    */

}

range & close

  • close用来关闭信道,应该由发送方关闭信道,只有在不再发送消息的时候才应该关闭。
  • range可以用来阻塞遍历信道接收到的值,直到信道关闭才会退出。
func fibo(n int, c chan int) {
    cur, pre := 0, 1
    for i := 0; i < n; i++ {
        time.Sleep(100 * time.Millisecond)
        c <- cur
        cur, pre = pre+cur, cur
    }
    close(c)
}

func TestCloseRange() {
    c1 := make(chan int)
    go fibo(10, c1)
    for {
        val, ok := <-c1
        if ok {
            fmt.Println(val)
        } else {
            // ok为false,表示信道关闭
            fmt.Println("end1.")
            break
        }
    }

    c := make(chan int)
    go fibo(10, c)
    for i := range c {
        fmt.Println(i)
    }
    fmt.Println("end.")
}

select多信道通信

信道是单向,这意味着两个线程要双向通信就必须由两个信道,而信道的发送和接收都会发生阻塞,所以需要一个工具协调发送和接收,select正是用于此。

func TestSelect() {
    send := make(chan string)
    reciver := make(chan string)

    go func() {
        for {
            // 线程内接收到信号
            val := <-send
            fmt.Println("thread receive value:", val)
            // 当接收到10,发送end信号
            if val == "10" {
                reciver <- "end"
                return
            }
        }
    }()

    x := 0
    for {
        var val string
        select {
        case val = <-reciver:
        // 接收另外线程的信号
            fmt.Println("main reciver value:", val)
        case send <- strconv.Itoa(x):
            // 不断发送信号
            x += 1

        }
        // 接收到end信号中断
        if val == "end" {
            break
        }
    }

}

Mutex互斥锁

在多线程操作中,比较危险的操作是多个线程同时访问一份数据,这可能导致不可预期的结果,所以为了解决这一类问题,golang也提供了锁,被锁锁定的数据同一时间只能被某个线程修改或访问。

type SafeCounter struct {
    m sync.Mutex
    c map[string]int
}

func (sc SafeCounter) Inc(val string) {
    sc.m.Lock()
    defer sc.m.Unlock()
    sc.c[val]++
}

func (sc SafeCounter) Value(val string) int {
    sc.m.Lock()
    defer sc.m.Unlock()
    return sc.c[val]

}

func TestMutex() {
    sc := SafeCounter{c: make(map[string]int)}

    for i := 0; i < 10; i++ {
        go sc.Inc("hello world")
    }
    time.Sleep(time.Second)
     // 如果没有锁,最后的结果可能不是10
    fmt.Println(sc.Value("hello world"))
}

总结

总体来说,golang比较简洁,学习曲线也不高,这与它的较低的年龄和设计人员的克制是分不开的。部分开发者认为其太过简单,不能形成技术壁垒,学习价值不大。对此,我深不以为然,在满足需求的前提下,越简单才是越好的。语言只是工具,技术人员的技术壁垒应该体现在使用语言去干出一般人干不到的事情。