go语言快速入门指北

0 前言

本文是个人自用的go语言指南学习笔记,主要是方便我个人复习。

通过上面那个指南,对于有编程基础的同学,可以在三天内速成go语言(我只花了两天

0.1 推荐学习资料

  1. 基于VSCODE的 go 环境搭建
  2. go语言指南--------适合快速入门
  3. topgoer-----我个人比较推荐这个指南
  4. go语言圣经--比较全面

1 包、函数、变量

1 包

  1. Go程序由包组成的,main程序对应一个package main
  2. Go中要导出的东西,以首字母大写命名,比如你要使用math包中的pi,那么这个东西要写成math.Pi

1.2 函数

1.2.1 函数

  1. 标准写法,支持多值返回

    func add_sub(x int, y int) (int,int) {
        return x + y,x-y
    }
    
  2. 命名返回值

    // 返回声明中定义了两个变量,可以直接在函数体内使用
    func split(sum int) (x, y int) {
    	x = sum * 4 / 9
    	y = sum - x
    	return
    }
    

1.2.2 变量

  1. 变量定义

      //1. 标准做法
      var a int = 0
      //2. 自动推导
      var a = 100
      //3. 同类型变量
      var a,b,c int
      //4. 海象运算符偷懒,省略一个var关键字,但是不能在函数外使用
      a := 100
      //5. 偷懒
      var (
      	a,b,c int = 1,2,3
          d bool = true
          e complex128 = cmplx.Sqrt(-5+12i) 
      )
    
  2. 常量 用const修饰

    const (
    	Big = 1 << 100
    	Small = Big >> 99
    )
    

2 流程控制语句

go控制流语句(if/for...)没有小括号,但是一定有大括号,而且大括号一定是不换行党直接杀死异教徒了属于是

2.1 循环

  1. go中只有for循环,写法类似c中的for,但是没有括号
  2. go中用for代替c中的while,同样是没有括号for i < 10
  3. 无限循环for{}

2.2 if-else

func pow(x, n, lim float64) float64 {
	if v := math.Pow(x, n); v < lim {	// 允许在if条件前写一个表达式(作用域在if中)
		return v
	}
	return v
}

2.3 switch

  1. go的switch中,可以不用写break(自动给你加上了)
  2. switch的case无需为常数,也不用为整数
func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os {
	case "darwin":
		fmt.Println("OS X.")
	case "linux":
		fmt.Println("Linux.")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.\n", os)
	}
}
  1. 无条件switch,替代if-then-else
func main() {
	t := time.Now()
	switch {
	case t.Hour() < 12:
		fmt.Println("Good morning!")
	case t.Hour() < 17:
		fmt.Println("Good afternoon.")
	default:
		fmt.Println("Good evening.")
	}
}

2.4 defer

defer:推迟函数调用,推迟的函数调用会被压入一个栈中。当外层函数返回时,被推迟的函数会按照后进先出的顺序调用。

defer求值的顺序是不变的,但是返回次序会发生变化

//猜猜下面代码的返回值
func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
		defer fmt.Println(i)
	}

	fmt.Println("done")
}

3 更多类型

3.1 指针

var p *int,p的用法和c一样,空指针的值为nil

3.2 结构体

type Vertex struct {
	X, Y int
}

// 初始化
var (
	v1 = Vertex{1, 2}  // 创建一个 Vertex 类型的结构体
	v2 = Vertex{X: 1}  // Y:0 被隐式地赋予
	v3 = Vertex{}      // X:0 Y:0
	p  = &Vertex{1, 2} // 创建一个 *Vertex 类型的结构体(指针)
)

3.3 数组

  1. 最基本的数组var a [10]int,其中长度也属于类型的一部分,也就是说[9]int,[10]int是两种不同类型

3.3.1 切片

切片是对数组的动态引用,类型为[]T

  1. 切片的长度与容量

    容量是指:从这个切片的第一个元素,到底层数组的最后一个元素的大小

    // 下面这个创建方法,会先创建一个数组,然后s引用这个数组
    // 类似先执行了 var tmp = [5]int{1,2,3,4,5}
    // 再执行了 s:=tmp[:]
    s := []int{1,2,3,4,5}		// len(s)=5 cap(s)=5
    
    s = s[:0] 		// len(s)=0 cap(s)=5		//并不会覆盖原来的s,新的s是一个引用
    s = s[:4]		// len(s)=4 cap(s)=5	扩展切片的大小,会覆盖那个引用
    s = s[2:]		// len(s)=2 cap(s)=3	舍弃前两个元素
    
  2. 修改切片的值,会同时改变底层数组的值,其他切片引用也会修改

  3. 切片的零值是 nil。nil 切片的长度和容量为 0 且没有底层数组。

3.3.2 动态数组的创建 ---make

 ```GO
 b := make([]int, 0, 5) // len(b)=0, cap(b)=5
 //make(type,len,cap),最后一个可以忽略,此时len=cap
 ```

3.3.3 append 切片扩容

发现一个很逆天的东西,解释在这里

func main() {    
    s := []int{1,2}    
    s = append(s, 3,4,5)    
    println(cap(s))
}

3.3.4 range

用range遍历的时候,实际上会返回两个值:元素的下标和对应的元素

var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}

func main() {
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
}

3.4 映射(kv对)

  1. map类型声明:map[键的类型]值的类型
// 定义一个结构体
type Vertex struct {
	Lat, Long float64
}

// 初始化创建一个map
var m = map[string]Vertex{
	"Bell Labs": Vertex{
		40.68433, -74.39967,
	},
	"Google": Vertex{
		37.42202, -122.08408,
	},
}

//下面写法更加简洁 
var m = map[string]Vertex{
	"Bell Labs": {40.68433, -74.39967},
	"Google":    {37.42202, -122.08408},
}
  1. 可以用make创建一个空map:var m = make(map[string]Vertex)
  2. 删除一个映射的元素:delete(m,key)
  3. 双赋值检测某个键是否存在elem,ok := m[key],若 keym 中,oktrue ;否则,okfalse

3.5 函数做参数or返回值

// 函数做参数
func compute(fn func(float64, float64) float64) float64 {
	return fn(3, 4)
}

func main() {
	hypot := func(x, y float64) float64 {
		return math.Sqrt(x*x + y*y)
	}
	fmt.Println(hypot(5, 12))

	fmt.Println(compute(hypot))
	fmt.Println(compute(math.Pow))
}
  1. compute函数的参数中,定义了一个func(float64,float64)float64类型的函数,参数中,将该函数命名为fn
  2. 主函数中hypot函数,首先创建了一个匿名函数,然后再赋值给这个hypot变量的

3.6 函数闭包

go的闭包

4 方法和接口

4.1 方法

  1. go没有类,但是可以为结构体定义方法,方法就是带有特殊接收者的函数
//下面是一个方法
type Vertex struct {
	X, Y float64
}

// 定义方式:func (方法接收者参数列表) 方法名(方法的参数列表) 返回类型{}
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
  1. 很神奇的是,你可以为非结构体类型声明方法,比如type float64 Myfloat,然后让这个Myfloat作为接收者

  2. 接收者的类型定义和方法声明必须在同一包内,所以如果直接使用float64为接收者,这是不允许的

    因为float64这种内建类型,不在你的包内定义,但是你可以用type重新命名一个

4.1.1 指针接收者

// 值接收者,不会修改接收者本身
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

// 指针接收者,会修改指针指向的接收者
func (v *Vertex) Scale(f float64) {
	v.X = v.X * f
	v.Y = v.Y * f
}

上面的例子类似函数的指针传参or值传参的过程,但是又有不同,不同在于 方法有指针的重定向

4.1.2 方法与指针的重定向

对于函数,值传参和指针传参是严格区分的

var v Vertex
ScaleFunc(v, 5)  // 编译错误!
ScaleFunc(&v, 5) // OK

对于方法,以指针为接收者的方法被调用时,接收者既能为值又能为指针:

// Scale是一个指针接收者的函数
var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK

对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)

同样的,当方法接收者为 值 的时候,即使你传入一个指针接收者,会通过编译

// Abs()是值接收者的函数
var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK,会被自动解释成(*p).Abs()

4.2 接口

go中的接口类似rust中的trait,但是go中将接口认为是一种类型,表示一组方法签名定义的集合。

接口类型的变量可以保存任何实现了这些方法的值。

func (v *Vertex) Abs() float64 {
	return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

type Abser interface {
	Abs() float64		// 方法名+返回类型
}

var a Abser
f := MyFloat(-math.Sqrt2)	// f是一个MyFloat变量
v := Vertex{3, 4}			// v是一个vertex变量

a = f  // a MyFloat 实现了 Abser
a = &v // a *Vertex 实现了 Abser

// 可以直接调用接口
a.Abs()

接口的隐式实现

一个类型通过实现一个接口的所有方法来实现该接口。这种方式就是隐式实现。

像rust中,要为某个类型实现某个trait时,要显式声明impl xxxtrait for xxx,go通过隐式实现就能完成某个接口,所以不需要有类似impl的关键字。

接口可作为值进行传递

从底层数据结构来看,接口实际上由(value,type)组成,value保存了一个具体type的具体值

type I interface{
    M()
}

// 为*T类型实现该接口
func (t *T) M() {
	fmt.Println(t.S)
}

func main(){
	var i I		//创建接口变量
    i = &T{"Hello"}		// 将i指向T类型的指针
    fmt.Printf("(%v,%T)\n",i,i)
}

输出结果为(&{Hello}, *main.T)

底层值为 nil 的接口值

// 下面的例子中,值为nil,但是type还是有的,整个接口变量也不为空
var i I
var t *T	// 定义一个*T类型,但是没有具体值
i=t
fmt.Printf("(%v,%T)",i,i)

输出结果:(, *main.T)

nil接口值

var i I
fmt.Printf("(%v, %T)\n", i, i)

此时的结果为(, )

空接口

指定了零个方法的接口值被称为 空接口

var i interface{}

// 空接口可保存任何类型的值,下面的赋值都没有问题
i = 52
i = "hello"

// 空接口被用来处理未知类型的值,比如下面这个函数,你传入任意数量的参数都行
func describe(i interface{}) {
	fmt.Printf("(%v, %T)\n", i, i)
}

类型断言

  1. 是一种 访问接口底层值的方式

    当我们认为接口变量i存在某个具体类型T的值时,我们可以通过t:=i.(T)来取得 对应该类型 的具体值。

    如果这个接口i,不存在我们想要的类型T 的具体值,就会引发panic

    所以说,这个过程就叫 断言,(我觉得可以理解为猜测)

  2. 类型判断

    类型断言可返回两个值,通过双返回的方法,不会引发panic

    t, ok := i.(T)

    i真的有T这个类型的值,那么会返回具体值,同时ok=true

    i没有这个类型的值,那么t=该类型的空值ok= false

类型选择

是一种按顺序从几个类型断言中选择分支的结构。

func do(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Printf("Twice %v is %v\n", v, v*2)
	case string:
		fmt.Printf("%q is %v bytes long\n", v, len(v))
	default:
		fmt.Printf("I don't know about type %T!\n", v)
	}
}

func main() {
	do(21)
	do("hello")
	do(true)
}

4.3 常用接口

4.3.1 Stringer

这是fmt包中常用的接口,Stringer 是一个可以用字符串描述自己的类型。fmt 包(还有很多包)都通过此接口来打印值。

// Stringer接口要包含一个String()方法
type Stringer interface {
    String() string
}

// 下面是一个例子
// 自定义了一个Person结构体
type Person struct {
	Name string
	Age  int
}

// 为自定义结构体定义 打印时如何输出
func (p Person) String() string {
	return fmt.Sprintf("%v (%v years)", p.Name, p.Age)
}

func main() {
	a := Person{"Arthur Dent", 42}
	z := Person{"Zaphod Beeblebrox", 9001}
	fmt.Println(a, z)		// 可以直接调用fmt的函数来打印我们自定义的结构体
}

4.3.2 错误 error

一个内建接口

// 该函数会返回一个error值,如果error值为nil,则无错误,如果不为nil,则发生了错误
type error interface {
    Error() string
}

4.3.3 Reader

io 包指定了 io.Reader 接口,它表示从数据流的末尾进行读取。

这个接口有很多用途,包括文件,网络连接等等

io.Reader 接口有一个 Read 方法:

func (T) Read(b []byte) (n int, err error)

Read 用数据填充给定的字节切片并返回填充的字节数和错误值。在遇到数据流的结尾时,它会返回一个 io.EOF 错误。

4.3.4 图像

image 包定义了 Image 接口:

// 具体是什么,查文档吧
package image

type Image interface {
    ColorModel() color.Model
    Bounds() Rectangle
    At(x, y int) color.Color
}

5 并发

5.1 Go程(goroutine)

Go 程(goroutine)是由 Go 运行时管理的 轻量级线程

// 下面的实例,会启动一个新的go程并执行f(x,y,z)函数
go f(x, y, z)

f,x,y,z的求值是在当前go程中,f函数的执行则是在新的go程中

5.2 信道

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。(箭头表示数据流的方向)

// 创建信道
ch = make(chan int)

// 数据传输
ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

5.2.1 带缓冲的信道

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

5.2.2 range 和 close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。

只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

之后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

一般来说信道不需要关闭,但是对于range循环,只能通过关闭来终止

5.2.3 select操作(多路复用)

select 语句使一个 Go 程可以等待多个通信操作。

通道(channel)实现了多个goroutine之前的同步或者通信,那么select则实现了多个通道(channel)的同步或者通信,并且select具有阻塞的特性。

select 随机执行一个可运行的 case(并发的)。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。

// select后面的每个case都必须是一个channel通信操作
select {
    case <-ch1:
        // 如果从 ch1 信道成功接收数据,则执行该分支代码
    case ch2 <- 1:
        // 如果成功向 ch2 信道成功发送数据,则执行该分支代码
    default:
        // 如果上面都没有成功,则进入 default 分支处理流程
}

go中的select实际上是参考了unix中的select,多路复用。区别就是unix中是对文件句柄读写的监控,而go中的select是对信道读写的监控

  1. 如果有超时条件语句,判断逻辑为如果在这个时间段内一直没有满足条件的case,则执行这个超时case。如果此段时间内出现了可操作的case,则直接执行这个case。一般用超时语句代替了default语句
  2. 对于空的select{},会引起死锁

5.2.4 sync.Mutex互斥锁类型

用于访问共享资源的时候进行互斥,有Lock和Unlock两个方法

我们可以用 defer 语句来保证互斥锁一定会被解锁。

posted @ 2023-07-02 10:35  wenli7363  阅读(52)  评论(0编辑  收藏  举报