go基础部分三 | 青训营笔记

83 阅读9分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 5 天

今天和大家分享go基础的第三部分。主要包括函数,方法,接口,协程的概念和常见操作。

函数

func main() {
    fmt.Println(fn(1, 2))
​
}
​
func fn(x int, y int) int {
    return (x + y)
}
​
// 3

上述代码中可以看出,go语言的函数声明顺序是随意的

同时函数的参数也可以是可变参数:

func main() {
    fmt.Println(show("x", "y", "z"))
​
}
//当使用 ...string类型时,表明读入的参数是一个切片
func show(args ...string) int {
    sum := 0
    for _, v := range args {
        fmt.Println(v)
        sum++
    }
    return sum
}
// x y z 3  

函数可以返回多个参数:

func main() {
    n, str := show("a", "b", "c")
    fmt.Println(n, str)
​
}
​
func show(args ...string) (int, string) {
    sum := 0
    str := ""
    for _, v := range args {
        sum++
        str += v
    }
    return sum, str
}
//3 abc

函数的返回值可以自带名称,这样函数就会自动去寻找对应的变量并返回,例如,上述函数可以改成:

func show(args ...string) (sum int, str string) {
    for _, v := range args {
        sum++
        str += v
    }
    return
}

这样,执行代码也会返回3 abc

函数没有名字则变成了匿名函数,go语言不允许函数嵌套,但是我们可以利用匿名函数来实现相同效果:

n,s := func (args ...string) (sum int, str string) {
            for _, v := range args {
                sum++
                str += v
            }
            return
        }("a", "b", "c")

函数可见性

  • 首字母大写,对于所有包时public,其他包任意调用
  • 首字母小写,这个函数是private,其他包无法访问

方法

方法和函数很类似,它可以通过附加行为来增强类型,方法在func这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体或者是非结构体类型。接收器是可以在方法内部访问的。

func (t Type) methodName(parameter list) {
​
}
t.methodName(parameter)

go不允许相同名字的函数,但是允许相同名字的方法绑定在不同的结构体中。

go的接收器可以使用指针或者值,如果我们想改变结构体的值,那么我们就需要使用到指针接收器:

//两个方法都可以在内部修改lesson的值,但是当外面实例化出来的结构体变量,只能通过第二种方式进行修改
func (lesson Lesson) AddOne() {
    lesson.num++
}
func (lesson *Lesson) AddOne2() {
    lesson.num++
}

同时,不适用指针的方法,仍可以使用指针去调用,这样做go会自动进行解引用:

func (lesson Lesson) AddOne() {
    ...
}
var l Lesson
(&l).AddOne() // 这样是可以的

接口

简介

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

type interface_name interface {
  method1()
  method2()
}

接口实例:

package main
​
import (
    "fmt"
)
​
type Phone interface {
    call()
}
​
type NokiaPhone struct {
}
​
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}
​
type IPhone struct {
}
​
func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}
​
func main() {
    var phone Phone
​
    phone = new(NokiaPhone)
    phone.call()
​
    phone = new(IPhone)
    phone.call()
​
}

可以利用接口来实现多态。

实现接口

以fmt包为例,其有一个Stringer接口(包含一个函数String),我们只要实现了他的这个接口,那么我们就可以利用该String函数的返回值给Printf,Println等打印函数所用:

/**
* fmt包的一个接口
* type Stringer interface {
*   String() string
* }
**/
type Location struct {
    x float64
    y float64
}
​
func (l Location) String() string {
    return "x: " + strconv.FormatFloat(l.x, 'f', 6, 64) + " y: " + strconv.FormatFloat(l.y, 'f', 6, 64)
}
​
func main() {
    location := Location{
        x: 1.0,
        y: 2.0,
    }
    fmt.Println(location) // x: 1.000000 y: 2.000000
}

泛型(利用空接口)

同时,利用空接口,我们可以实现可以承载任何类型的变量:

func main() {
    a := make([]interface{}, 3)
    a[0] = "1"
    a[1] = 2
    a[2] = func() {
        fmt.Println("abc")
    }
    fmt.Println(a) //[1 2 0x1089780]
  a[2].(func())() //这里使用来接口的断言,让go知道当前的类型是方法,我们就可以直接调用匿名方法了
}

接口的嵌套(集成)

type Device interface {
    on()
} 
type Phone interface {
  Device  
  call()
}

接口的nil

interface 是一个特殊结构,它由两部分组成,(type, value),当我们直接声明一个变量为某个interface类型时,他是nil,此时其内部(nil, nil)

例如: var s interface{}

当我们给他制定一个值时,有三种情况:

第一种是只声明了接口:

var s fmt.Stringer  //fmt.Stringer 是一个接口,里面存在String()方法

那么此时s打印出来的是nil

第二种确定了接口类型但值为空,例如:

type Person struct {
 name string
}
func (p Person) String() string {
 return p.name
}
var p *Person // p是一个指针,没有初始化所以为nil
var s fmt.Stringer = p

那么此时s内部为 (*Person, nil),打印出来还是nil

第三种情况 指定的变量初始化了

var p *Person = &Person{
  name: "xxx",
}

那么此时s内部为(*Person, value),打印出来的就不是nil了。

go认定,接口只有类型和值都为nil才等于nil,所以会出现下面这种情况:

func main() {
    var v interface{}
    fmt.Printf("%T %v %v\n", v, v, v == nil) // <nil> <nil> true
    var p *int
    v = p
    fmt.Printf("%T %v %v\n", v, v, v == nil) // *int <nil> false  虽然值为nil,但是确定了类型,所以不等于nil
    fmt.Printf("%#v\n", v) // (*int)(nil)
}

go的流程控制

if else

if a > 1 {
    // ...
}else if a < 0{
    // ...
} else {
    // ...
}
​
/*if 可执行语句; 判断 {
  ...
}*/
if a := getNum(); a > 1 {
 // ...
}

if 可以包含一个初始化语句(如:给一个变量赋值)。这种写法具有固定的格式(在初始化语句后方必须加上分号):

if initialization; condition {
    // do something
}

例如:

val := 10
if val > max {
    // do something
}

你也可以这样写:

if val := 10; val > max {
    // do something
}

但要注意的是,使用简短方式 := 声明的变量的作用域只存在于 if 结构中(在 if 结构的大括号之间,如果使用 if-else 结构则在 else 代码块中变量也会存在)。如果变量在 if 结构之前就已经存在,那么在 if 结构中,该变量原来的值会被隐藏。最简单的解决方案就是不要在初始化语句中声明变量。

switch

switch 表达式 {
    case 值:
        执行代码
  case1,值2, 值3:  //多条件判断
        执行代码
    default:
    执行代码
}
​
/*switch statement; expression {}*/
switch a:= getNum(); a {
    case 1
        执行代码
  case 2, 3:  //多条件判断
        执行代码
    default:
    执行代码
}

go的switch,默认自带break,若需要执行后面的case,可以使用 fallthrough

switch 表达式 {
    case 值:
        执行代码
        fallthrough //若满足了上面的case,则也执行后面的执行代码
  case1,值2, 值3:  //多条件判断
        执行代码
        fallthrough
    default:
    执行代码
}

switch还有另一种写法:

var ans = getAns()
switch {
case ans == "a":
    fmt.Println("ans is a")
case ans == "b":
    fmt.Println("ans is a")
case ans == "c":
    fmt.Println("ans is a")
default:
    fmt.Println("there is no ans")
}

for

for i := 0; i < count; i++ {
        
}
​
//for range 形式
for index, value := range arr {
        
}
//类似于while
for num<4 {
  
}

go是没有while的,可以使用for来替代

defer延迟调用

在函数名或者结构的方法前加上defer,可以让函数延迟执行。

defer栈:

多个defer存在时,会采用栈的方式存储和调用,即最后一个defer函数会被首先执行(但也是延迟到其他函数执行完之后)。

goto

表示我们下一步要去执行哪里的代码:

    fmt.Println("xxxx")
    goto label
    fmt.Println("yyyy")
label:
    fmt.Println("zzzz")
//  xxxx zzzz

注意,goto和label之间不能有变量声明,否则会报错

协程(Coroutine)

Go语言的协程是与其他函数或者方法一起并发运行的工作方式。协程可以看做是轻量级线程。与线程相比,创建一个协程的成本很小。

开启一个协程:

package main
​
import (
    "fmt"
    "time"
)
​
func main() {
    go PrintInfo()
    time.Sleep(1 * time.Second) //让主协程也就是main函数歇一会,不然主协程终止,整个程序也就终止了
    fmt.Println("hello ")
}
func PrintInfo() {
    fmt.Println("hello go")
}

协程转让

runtime.Gosched()这个函数的作用是让当前goroutine让出CPU,好让其它的goroutine获得执行的机会。同时,当前的goroutine也会在未来的某个时间点继续运行。

多协程检测访问冲突

如果又多个协程同时执行,可能会出现资源访问冲突,我们可以使用go run -race xxx 来检测

通道

go协程之间通讯的管道,它是一种队列的数据结构

通道的声明:

/*
通道的声明 chan 就是channel的缩写
var channel_name chan channel_type
*/
var ch chan string
ch = make(chan string)
​
//或者
ch := make(chan string)

通道的使用:

通道可以在协程之间传递数据,并且起到阻塞的作用:

func main() {
    var ch chan string = make(chan string)
    fmt.Println("1")
    go PrintChan(ch)
    res := <-ch // 发生了阻塞,等待ch返回结果后才会向下执行
    fmt.Println(res)
    println("3")
}
​
func PrintChan(c chan string) {
    c <- "2"
}
​
/* 1
   2
   3 */

可以看出,这次的main主协程,并没有运行到底,而是等待ch返回结果后才继续执行。

注意 这里的通道由于没设置长度,我们需要等到有两两个不同的协程(包括主进程)来同时有存和取时才可以执行。否则将一直阻塞或者产生死锁(当所有另开协程的协程都睡了,但是主进程还要操作通道时)

关闭通道:

close(ch)
//检测通道情况
value, ok := <-ch // 如果通道已经关闭,则ok为false

通道的长度和容量:

通道可以利用make函数来设置长度:make(chan typeName, length)的方式设置

c := make(chan int, 3) //make一个长度为3的通道,当长度设置为0时,则称为无缓冲通道,存取必须同步

当通道没有存储数据时,其cap容量为0,使用c <- data数据时,其容量加1;使用<- c取出数据时,其容量减1。当通道的容量等于长度时再次进行存储数据,就会发生阻塞的情况。

所以我们尽量上通道的存取是同步的。

对通道的遍历:

func main() {
    var ch = make(chan int, 5)
    go loopFn(ch)
    for v := range ch {
        fmt.Println(v)
    }
}
​
func loopFn(c chan int) {
    for i := 0; i < 10; i++ {
        c <- i
    }
    close(c)
}
// 0 1 2 ... 9

这里由于协程之间的通道写进去就被读出来,所以没有产生阻塞,也可以看出,即使通道被close关闭了,仍可以取出数据。

我们可以利用,容量为1的通道,通过堵塞的效果,达到锁的作用:

var ch = make(chan bool, 1)
​
ch <- true //产生堵塞
a = a + 1  //同一时间我们只希望一个协程去操作这个a = a + 1
<- ch //消除堵塞