今天是个好天气 logo 今天是个好天气
  • Home
  • Go
  • MySQL
  • Redis
  • LeetCode
  • Hello World
↩️README 配个环境 基础语法 并发编程 脚手架使用 RPC 数据结构 逃逸分析

Hello world

Go语言使用

获取远程包

go get github.com/avb/asd

go get -u //自动更新包

Go Moudle

如何使用

  • go mod tidy会检测import中缺少的包,进行下载
  • go get roc.io/quote@v1.9.1 指定版本号
  • 再执行一次go mod tidy 可以去除不必要的包

发布步骤

module github.com/shixiaocaia/Learn_go

go 1.20

require rsc.io/quote v1.5.1

require (
	golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c // indirect
	rsc.io/sampler v1.3.0 // indirect
)

Go 模块准备好后,只需简单几步,即可将其发布出去:

  1. 打开命令行并进入模块根目录;

  2. 执行 go mod tidy 命令,删除一些不必要的模块依赖;

    我们演示发布的模块没有引用任何依赖,因此这步可有可无;

  3. 执行测试用例,最后一次确认代码运行无误;

     go test ... 
    

    go test 命令执行模块的单测用例,用例可以用 Go 语言的测试框架来写。我们演示的模块没有测试用例,因此这步也可以跳过。

  4. 执行 git tag 命令,用新版本号打标签;

    # 提交代码
    git commit -m "first version"
    
    # 打标签
    git tag v1.0.0
    

    版本号必须符合模块版本命名规则,以便让用户知晓代码变更情况(接口是否兼容)。

  5. 将新标签推到原仓库(如 Github );

    git push origin v1.0.0
    
  6. 执行 go list 命令触发 Go 更新模块索引信息,确保新发布的模块可用;

    GOPROXY=proxy.golang.org go list -m github.com/fasionchan/goutils@v1.0.0
    

    注意到,我们在命令之前设置了 GOPROXY 环境变量,指定使用 proxy.golang.org 作为 Go 代理。这可以确保我们请求到官方代理,而不是各种加速镜像。

数据结构

变量定义

  • 类型声明

    • type FreezingC float64
      type Fahrenheit float64
      
    • FreezingC Fahrenheit虽然底层都相同,都是float64,但是仍然不是同一种类型,进行计算时需要显示类型转换

    • Go中不具有隐式类型转换

  • 全局变量如果要在包外访问,首字母需要大写,golang是以首字母大小写来区分对包外是否可见。

常量

const str = "hello"
const(
    e = 1.2323
    p = 2312
)
  • 常量不能用 :=
  • 保证在编译阶段就计算出表达式的值,不需要等到运行阶段。
  • 常量并不从属于任何具体类型,所以常量在声明时不说类型。

字符串

s := "hello world"
var s string = "hello world"
s2 := s[0:5] // 创建[0,5)的子串
  • Go中区别" " 和 ' ' 前者表示string,后者表示byte类型。
  • Go中string相当于只读byte类似数组,需要转换为[]byte 字节切片。
    • 两个字符串通过+=语句修改,是将新字符串赋值给变量
    • 两个字符串可以安全共用一段底层内存
    • 当发生+=,实际上是指向了新的内存地址,原来的底层没有发生变更
  • string和byte不能直接运算拼接,需要使用显示类型转换。

rune

在Go语言当中,字符的概念被称为 rune - 它是一个表示 Unicode 编码的整数。

指针

var a int = 5
p := &a

var a *int
  • 用&取地址,用*取地址中的值
  • 在打印时,用&特殊标记,这是一个指针变量。
  • 当你传值给函数或方法时,Go 会复制这些值。因此,如果你写的函数需要更改状态,你就需要用指针指向你想要更改的值

数组

array := [3]string{"heli,"hello","world"}

array := [...]string{"heli,"hello","world"}

func modify(array [5]string){
    array[0] = "another"
}

modify(array)

a := [2][3]int{
    {1, 1, 1},
    {2, 2, 2}
}
  • 初始化后长度不可变
  • 数组的长度len和空间cap,一定是相等的
  • 数组在Go中为值类型
  • 长度不同的数组,是不同的类型

Slice

slice := []string{"heli,"hello","world"}


newSlice := make([]int, 2)
// len = cap = 2

newSlice := make([]int, 2, 100)
// len = 2, cap = 100

multSlice := make([][]int, 0)

multSlice = append(multSlice, []int{1,2,3})
multSlice = append(multSlice, []int{2,2,3})
multSlice[0][1] = 100
  • Slice是一个结构体,指向底层的数组
  • 区别一般数组,是否指定数组的大小。是一种动态数组,长度可变
    • cap:申请的空间
    • len:实际使用的空间
    • 扩容:cap == len时,对cap进行翻倍扩容,重新分配内存地址,但是减小大小,不会改变地址
  • slice不能直接比较,range一个比较或者使用reflect.DeepEqual
    • 注意reflect.DeepEqual是类型不安全的
    • 比较string和slice,能通过编译是不合理的

Byte

  • uint8的别名

Map

// 创建一个变量
var m map[int]string = make(map[int]string)

m := map[sting]int{
    "ming":10,
    "zhangsan":13,
}

// 二维
var m map[int]map[int]string
m = make(map[int]map[int]string)
// 分布初始化
m[1] = make(map[int]string)
m[1][1] = "OK"

a,ok := m[2][1]
if !ok{
    m[2] = make(map[int]string)
}


score, err := m["ming"]
//访问时返回值和是否存在(true or false)

//注意这边的value知识一个拷贝,不对实际产生影响
for key, value := range m{
    fmt.Println("%s\t %d\t", key, value)
}

// 删除
delete(m, "ming")
  • 存储一系列无序的键值对,基于键来存储值
  • 传参时,还是指针,与切片相等,会改变底层的内容
  • 无序访问

Struct

type person struct{
    Name string
    Age int
}

func main() {
    //a := person{}
    //a.Name = "joe"
    //a.Age = 19
    a := &person{
        Name: "joe",
        Age: 19
    }
    a.Age = 20
}

// 匿名结构
func main(){
    a := & struct{
        Name string
        Age
    }{
        Name: "joe",
        Age: 19
    }
    
}

普通传递是值传递,所以开始就加&,取地址,方便后续使用,避免每一次使用取地址

type human struct{
    Sex int
}

type teacher struct{
    human
    Name string
    Age int
}

a := teacher{Name: "joe", Age: 19, human: human{Sex:0}}
a.Sex = 1

支持嵌套

sm1 := struct {
    age int
    m   map[string]string
}{age: 11, m: map[string]string{"a": "1"}}

sm2 := struct {
    age int
    m   map[string]string
}{age: 11, m: map[string]string{"a": "1"}}

if sm1 == sm2 {
    fmt.Println("sm1 == sm2")
}
  • 只有相同类型的结构体才可以比较,结构体是否相同不但与属性类型个数有关,还与属性顺序相关.
  • 此外构体属性中有不可以比较的类型,如map,slice,则结构体不能用==比较。应该使用reflect.DeepEqual(s1,s2)

JSON

泛型

package main

import "fmt"

// K, V是类型参数
func MapKeys[K comparable, V any](m map[K]V) []K {
	r := make([]K, 0, len(m))
	for k := range m {
		r = append(r, k) // 复制key值
	}
	return r
}

type List[T any] struct {
	head, tail *element[T]
}

type element[T any] struct {
	next *element[T]
	val  T
}

// 使用时必须保留类型参数
func (lst *List[T]) Push(v T) {
	if lst.tail == nil {
		lst.head = &element[T]{val: v}
		lst.tail = lst.head
	} else {
		lst.tail.next = &element[T]{val: v}
		lst.tail = lst.tail.next
	}
}

func (lst *List[T]) GetAll() []T {
	var elems []T
	for e := lst.head; e != nil; e = e.next {
		elems = append(elems, e.val)
	}
	return elems
}

func main() {
	var m = map[int]string{1: "2", 2: "4", 4: "8"}

	fmt.Println("keys m:", MapKeys(m))

	// 可以不用指定类型参数
	_ = MapKeys[int, string](m)

	lst := List[int]{}
	lst.Push(10)
	lst.Push(13)
	lst.Push(23)
	fmt.Println("list:", lst.GetAll())
}

错误处理

package main

import (
	"errors"
	"fmt"
)

func f1(arg int) (int, error) {
	if arg == 42 {

		return -1, errors.New("can't work with 42")

	}

	return arg + 3, nil
}

type argError struct {
	arg  int
	prob string
}

func (e *argError) Error() string {
	return fmt.Sprintf("%d - %s", e.arg, e.prob)
}

func f2(arg int) (int, error) {
	if arg == 42 {

		return -1, &argError{arg, "can't work with it"}
	}
	return arg + 3, nil
}

func main() {

	for _, i := range []int{7, 42} {
		if r, e := f1(i); e != nil {
			fmt.Println("f1 failed:", e)
		} else {
			fmt.Println("f1 worked:", r)
		}
	}
	for _, i := range []int{7, 42} {
		if r, e := f2(i); e != nil {
			fmt.Println("f2 failed:", e)
		} else {
			fmt.Println("f2 worked:", r)
		}
	}

	_, e := f2(42)
	if ae, ok := e.(*argError); ok {
		fmt.Println(ae.arg)
		fmt.Println(ae.prob)
	}
}

  • 按照惯例,错误通常是最后一个返回值并且是 error 类型,它是一个内建的接口。
  • 返回错误值为 nil 代表没有错误。
  • 你还可以通过实现 Error() 方法来自定义 error 类型。 这里使用自定义错误类型来表示上面例子中的参数错误。
  • 在程序中使用自定义错误类型的数据, 你需要通过类型断言来得到这个自定义错误类型的实例。

控制语句

switch

func main(){
    a := 1
    switch a {
    case a >= 0:
         fmt.Println("a")
         fallthrough
    case 1:
    
    }

}
  • 如果switch不加语句块,希望判断每一个case条件,使用fallthrough
  • 默认下一个case满足,会跳出这个switch
  • 当有很多个if语句检查特定的值,通常用switch语句来代替

for range 引用

for _, url := range urls {
    go func() {
        results[url] = wc(url)
    }()
}
  • 注意这里url每次从urls获取新值,但是每个 goroutine 都是 url 变量的引用 —— 它们没有自己的独立副本。
for _, url := range urls {
    go func(u string) {
        results[u] = wc(u)
    }(url)
}
  • 用 url 作为参数调用匿名函数,我们确保 u 的值固定为循环迭代的 url 值,重新启动 goroutine。u 是 url 值的副本,因此无法更改。
  • 通过创建新变量赋值使用,而不是引用

函数

// 不定长变参,这是拷贝值
func A(b string, a ...int){
    

}

// 拷贝地址
func A(b string, a []int){


}
  • Go函数不支持嵌套、重载和默认参数
  • 无需声明原型、不定长度变参、多返回值、命名返回值参数
func main(){
    a := func(){
        fmt.Println("A")
    }
    a()
}

A 是一个函数类型变量,当作函数一样调用

func main(){
    f := closure(10)
    fmt.Println(f(1))
}
func closure(x int) func(int) int{
    return func(y int) int {
        return x + y
    }
}

返回值是一个函数,x是声明时给的值,y是后传入的值

package main

/*
    下面代码是否编译通过?
*/
func myFunc(x,y int)(sum int,error){
    return x+y,nil
}

func main() {
    num, err := myFunc(1, 2)
    fmt.Println("num = ", num)
}
  • 在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须命名
  • 如果返回值有有多个返回值必须加上括号
  • 如果只有一个返回值并且有命名也需要加上括号

Defer

  1. 延迟函数什么时候被调用

    • 函数return的时候
    • 发生panic时候
  2. 延迟调用的语法规则

    • defer关键字后面表达式必须是函数或者方法的调用
    • 延迟内容不能被括号括起来
  3. defer语句执行的顺序是先见后出LIFO


defer用于资源的释放,会在函数返回之前进行调用。一般采用如下模式:

f,err := os.Open(filename)
if err != nil {
    panic(err)
}
defer f.Close()

要使用defer时不踩坑,最重要的一点就是要明白,return xxx这一条语句并不是一条原子指令!

函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后才是返回到调用函数中。

func f() (result int) {
     // result = 0  //return语句不是一条原子调用,return xxx其实是赋值+ret指令
     func() { //defer被插入到return之前执行,也就是赋返回值和ret指令之间
         result++
     }()
     return
}

func f() (r int) {
     t := 5
     // r = t //赋值指令
     func() {        //defer被插入到赋值与返回之间执行,这个例子中返回值r没被修改过
         t = t + 5
     }
     return        //空的return指令 return t
}

func f(){
	var num = 1
    defer fmt.Printf("num is %d", num)
    
    num = 2 
    return
}

func deferRun()(res int){
    num := 1
    
    defer func() {
		res++
    }()
    return num
}
  • 例子1,返回值变量已经定义了,先复制result = 0,改变result值
  • 例子2,要返回的是r,执行一次r = t,再执行defer,实际不改变r的值
  • 例子3,defer出现时,参数num已经确定了,后面无法修改
  • 例子4,例子3中如果使用指针,传递时地址,地址没变,但是地址内的值可以发生改变
  • 例子5,这里先执行res = num = 1,再执行res++= 2
func deferRun() int{
    var num int
    defer func(){
      num++  
    }()
    
    return 1
}
  • return设置返回值res = 1,defer num+ 1,第三步将res返回,不受影响仍然是1
func deferRun() int{
    num := 1
    defer func(){
      num++  
    }()
    
    return num
}
  • 同样,先设置return res = num,对num影响不改变res

区别匿名函数和有名函数的,返回值的区别,记住先赋值、再defer、最后返回。

Panic

  • 单独panic发生后,后续不会执行,配合defer中包含recover函数进行恢复
    • 因为defer无论如何都会执行,所以发生panic后,把recover放到defer中
package main

import "fmt"

func Demo(i int) {
	//定义10个元素的数组
	var arr [10]int
	//错误拦截要在产生错误前设置
	defer func() {
		//设置recover拦截错误信息
		err := recover()
		//产生panic异常  打印错误信息
		if err != nil {
			fmt.Println(err)
		}
	}()
	//根据函数参数为数组元素赋值
	//如果i的值超过数组下标 会报错误:数组下标越界
	arr[i] = 10

}

func main() {
	Demo(10)
	//产生错误后 程序继续
	fmt.Println("程序继续执行...")
}

可变参数函数

package main

import "fmt"

func sum(nums ...int) {
    fmt.Print(nums, " ")
    total := 0
    for _, num := range nums {
        total += num
    }
    fmt.Println(total)
}

func main() {

    sum(1, 2)
    sum(1, 2, 3)

    nums := []int{1, 2, 3, 4}
    sum(nums...)
}

闭包

匿名函数在你想定义一个不需要命名的内联函数时是很实用的。

package main

import "fmt"

func intSeq() func() int {
    i := 0
    return func() int {
        i++
        return i
    }
}

func main() {

    nextInt := intSeq()

    fmt.Println(nextInt())
    fmt.Println(nextInt())
    fmt.Println(nextInt())

    newInts := intSeq()
    fmt.Println(newInts())
}
  • nextInt拥有属于自己的i,每次调用返回同一个i。

方法

是某种特定类型的函数。

  • 同名方法,不能根据形参的类型进行重载
  • 对于type定义,也可以使用附加特定的方法
var a TZ
//method value
a.Print()
//method expression
(*TZ).Print(&a)

func (a *TZ) Print(){
    fmt.Println("Tz")
}

interface与类型断言

一个数据通过func funcName(interface{})的方式传进来的时候,也就意味着这个参数被自动的转为interface{}的类型。

在我们使用时,应该进行断言

var a interface{}

fmt.Println("Where are you,Jonny?", a.(string))

但是直接断言,错误会发生panic,所以一般在断言前进行一定的判断

value, ok := a.(string)
value, ok := a.(string)
if !ok {
    fmt.Println("It's not ok for type string")
    return
}
fmt.Println("The value is ", value)

类型断言用于判断一个接口对象是否实现了特定的接口,并且获取其对应的实际值。在这段代码中,r.(io.Writer)是一种类型断言,用于判断r是否实现了io.Writer接口,并将其转换为io.Writer类型的实例赋值给变量w。

而类型强制转换是将一个数据从一种类型转换为另一种类型,这通常在非接口类型之间进行。例如,将一个整数类型转换为浮点数类型,或者将一个字符串类型转换为字节切片类型等。

接口

package main

import "fmt"

type USB interface {
    Name() string
    Connect()
}

type PhoneConnecter struct {
    name string
}

func (pc PhoneConnecter) Name() string {
    return pc.name
}

func (pc PhoneConnecter) Connect() {
    fmt.Println("Connect:", pc.name)
}

func main() {
    a := PhoneConnecter{name: "xiaocai"}
    a.Connect()
    Disconnect(a)
}

func Disconnect(usb USB) {
    fmt.Println("Disconnext.")
}
  • 一般把接口类型名后面加上er,接口作为函数参数时,任何满足这个接口的变量类型都可以传入
  • 接口是一个或多个方法签名的集合
    • USB这个接口含有name、Connect两个方法
    • 只有方法声明,没有实现,没有数据字段
  • 只要某个类型拥有该接口的所有方法签名,即算实现该接口,无需显示声明实现了哪个接口,这称为Structural Typing
    • PhoneConnecter实现了USB这个接口中的name,Connect方法,就算实现了该接口
type USB interface {
    Name() string
    Connecter
}

type Connecter interface {
    Connect()
}
  • 接口可以匿名嵌入其它接口,或嵌入到结构中
  • 也同时满足struct的嵌入,struct中嵌入一个满足接口类型的类型,那么整体新的struct也满足这个接口
func Disconnect(usb USB) {
    if pc, ok := usb.(PhoneConnecter); ok {
       fmt.Println("Disconnect:", pc.name)
       return
    }
    fmt.Println("Unknown device")
}
  • 对接口类型的断言
type empty interface{

}
  • 空接口,任何类型都实现了
func Disconnect(usb USB) {
    switch v := usb.(type) {
    case PhoneConnecter:
       fmt.Println("Disconnect:", v.name)
    default:
       fmt.Println("Unknown device")
    }
}
  • 当接口的类型过多时,使用type switch
type tvConnecter struct{
    name string
}

func (tv  tvConnecter)Connect{
    ...
}

tv := tvConnecter{"1234"}
var a USB
a = USB(tv) //error
  • 超级接口PhoneConnecter可以转换为下级接口,反之Connecter不可以转换为PhoneConnecter
    • 也就是只能实现降级转换
a := PhoneConnecter{name: "xiaocai"}

var tmp PhoneConnecter
tmp = a

tmp.Connect()

a.name = "pc"

tmp.Connect()
  • 将对象赋值给接口时,会发生拷贝,而接口内部存储的是指向这个复制品的指针,既无法修改复制品的状态,也无法获取指针
    • 这里tmp收到的是浅拷贝
    • a的改动,不会影响tmp
func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
    p := newPrinter()
    p.doPrintf(format, a)
    n, err = w.Write(p.buf)
    p.free()
    return
}
  • io.Writer 是一个很好的通用接口,用于「将数据放在某个地方」。

反射

是什么

反射是一种机制,在编译时不知道具体类型的情况下,可以透视结构的组成、更新值。使用反射,可以让我们编写出能统一处理所有类型的代码。

interface和反射

为什么不通过将所有参数都定义为 interface 类型来得到真正灵活的函数呢?

  • 作为函数的使用者,使用interface将失去对类型安全的检查,并且无法知道传入参数的类型。
  • 作为这样一个函数的作者,你必须检查传入的 所有 参数,并尝试断定参数类型以及如何处理它们。

如果你想实现函数的多态性,请考虑是否可以围绕接口(不是 interface 类型,这里容易让人困惑)设计它,以便用户可以用多种类型来调用你的函数,这些类型实现了函数工作所需要的任何方法。

编写测试

编写测试和函数很类似,其中有一些规则

  • 程序需要在一个名为 xxx_test.go 的文件中编写
  • 测试函数的命名必须以单词 Test 开始
  • 测试函数只接受一个参数 t *testing.T

现在这些信息足以让我们明白,类型为 *testing.T 的变量 t 是你在测试框架中的 hook(钩子),所以当你想让测试失败时可以执行 t.Fail() 之类的操作。

package main

import "testing"

func TestHello(t *testing.T) {

	// 将断言重构为匿名函数,减少重复,提高可读性
	assertCorrectMessage := func(t *testing.T, got, want string) {
		t.Helper() // 这是辅助函数,出错显示在调用地方,而不是辅助函数内部
		if got != want {
			t.Errorf("got '%q' want '%q'", got, want)
		}
	}

	// 子测试
	t.Run("saying hello to people", func(t *testing.T) {
		got := Hello("Chris", "")
		want := "Hello, Chris"
		assertCorrectMessage(t, got, want)
	})

	t.Run("empty string defaults to 'world'", func(t *testing.T) {
		got := Hello("", "")
		want := "Hello, World"
		assertCorrectMessage(t, got, want)
	})

	t.Run("in Spanish", func(t *testing.T) {
		got := Hello("Elodie", "Spanish")
		want := "Hola, Elodie"
		assertCorrectMessage(t, got, want)
	})

	t.Run("in French", func(t *testing.T) {
		got := Hello("Elodie", "French")
		want := "Bonjour, Elodie"
		assertCorrectMessage(t, got, want)
	})

}

  • 重构重复函数,提高代码可读性
  • 子测试,测试不同的案例
func ExampleAdd() {
	sum := Add(2, 2)
	fmt.Println(sum)
	// Output: 4
}
  • 在同一个包文件中添加_test.go,编写示例代码,提高可读性
  • 示例代码会作为测试套件的一部分,示例会被编译
  • 如果删除注释 「//Output: 6」,示例函数将不会执行。虽然函数会被编译,但是它不会执行。

基准测试

func BenchmarkRepeat(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Repeat("a")
    }
}

testing.B 可使你访问隐性命名(cryptically named)b.N。

基准测试运行时,代码会运行 b.N 次,并测量需要多长时间。

代码运行的次数不会对你产生影响,测试框架会选择一个它所认为的最佳值,以便让你获得更合理的结果。

用 go test -bench=. 来运行基准测试。 (如果在 Windows Powershell 环境下使用 go test -bench=".")

表格驱动测试

func TestArea(t *testing.T) {

    areaTests := []struct {
        shape Shape
        want  float64
    }{
        {Rectangle{12, 6}, 72.0},
        {Circle{10}, 314.1592653589793},
    }

    for _, tt := range areaTests {
        got := tt.shape.Area()
        if got != tt.want {
            t.Errorf("got %.2f want %.2f", got, tt.want)
        }
    }

}
  • 创建了一个匿名的结构体,用含有两个域 shape 和 want 的 []struct 声明了一个结构体切片。然后我们用测试用例去填充这个数组了。

反射

  1. Go语言使用
    1. 获取远程包
  2. Go Moudle
    1. 如何使用
    2. 发布步骤
  3. 数据结构
    1. 变量定义
    2. 常量
    3. 字符串
    4. rune
    5. 指针
    6. 数组
    7. Slice
    8. Byte
    9. Map
    10. Struct
    11. JSON
  4. 泛型
  5. 错误处理
  6. 控制语句
    1. switch
    2. for range 引用
  7. 函数
    1. Defer
    2. Panic
    3. 可变参数函数
    4. 闭包
  8. 方法
  9. interface与类型断言
  10. 接口
  11. 反射
    1. 是什么
    2. interface和反射
  12. 编写测试
    1. 基准测试
    2. 表格驱动测试
  13. 反射
Created by shixiaocaia | Powered by idoc
Think less and do more.