这是我参与「第五届青训营 」伴学笔记创作活动的第 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 值:
执行代码
case 值1,值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,则也执行后面的执行代码
case 值1,值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 //消除堵塞