Golang 的快速入门学习

Golang 的快速入门学习

一、Go语言结构

​ Go语言的基础组成有以下几个部分:包声明、引入包、函数、变量、语句&表达式以及注释。

package main

​ 定义包名,说明这个文件属于哪个包,按照上面这么写就表示是一个可独立执行的程序,每个Go程序都包含一个名为main的包。

import "fmt"

​ 说明该程序需要使用fmt包中的某些函数或者是元素,那fmt就是一个非常常用的包,实现了格式化IO的函数,比如fmt.Println输出到console。

func main()

​ 这是程序开始执行的函数,main函数是每个可执行的程序必须包含的,一般来说是启动后第一个执行的函数,如果有init则例外,因为init()函数会先于main开始执行。

​ 当标识符(常量、变量、类型、函数名、结构字段等等)以一个大写字母与开头,比如说Han1,那么这样形式的标识符的对象就可以被外部包的代码所使用(要注意客户端需要先导入其所在的包),有点类似于java中的public,反之如果是小写字母开头,那必然是对包外不可见,但是在整个包的内部是可见可用的,类似于protected

​ 从fmt.Println("hello world!")就可见一斑。

​ 下面给出一个helloworld的完整demo。

package main

import "fmt"

func main(){
    fmt.Println("hello world");
}

​ 哈哈哈,要注意main函数后面的大括号要紧跟着,{不要另起一行。

​ 如果需要运行的话:

  • go run filename.go,或许是最直白的方式;

  • 也可以生成二进制文件,每次通过./filename的方式执行。

    • go build filename.go
    • ./filename

    如何调试:

  • go run filename.go

关于包

​ 之前marco提到包名和文件名的概念来着,这里小结一下。

  • 文件名和包名没有直接关系,不一定要将文件名和包名定成同一个;

  • 文件夹名和包名也是没直接关系,并非需要一致;

  • 同一个文件夹下的文件只能有一个包名否则编译报错

二、 Go语言数据类型

  • 布尔:var b bool=true

  • 数字类型:整型int、浮点型,Go还支持复数。

  • 字符串类型:一串固定长度的字符连接起来的字符序列。

  • 派生类型:

    • Pointer
    • 数组类型
    • struct
    • Channel类型
    • 函数类型
    • 切片类型
    • interface类型
    • Map类型

    go 1.9版本对于数字类型,无需定义int以及float32、float64,系统会自动识别。

var isActive bool //全局变量声明
var enabled,disabled=true,false //忽略类型的声明
func test(){
    var available bool //一般声明
    valid := false //简短声明
    available = true //赋值操作
}

三、Go 语言变量

​ 变量声明:

  • 声明一个变量并初始化:var a="RUNOOB"

  • 声明一个变量,但没有初始化,则默认是零值

    ​ 几个零值为nil的类型:

var a *int
var a []int 
var a map[string] int
var a chan int 
var a func(string) int
var a error //此处err是个接口
  • 根据值自动判断变量的类型:var var_name=value

  • 省略varv_name := value,只能提出现在函数体中,相同的代码块中,不能对相同名称的变量使用初始化声明(会提示no new variables on left side of :=),在定义变量之前使用变量varname,会得到编译错误undefined:varname,如果声明了一个局部变量但是没有在相同的代码块中使用,也会得到编译错误(会提示a declared and not used)。

    package main 
    
    import "fmt"
    
    func main(){
        var a string ="abc"
        fmt.Println("hello world",a)
    }
    

    ​ 上面的这种方式就可以移除错误。

    var intVal int
    intVal :=1   x错误,:=是个声明语句,然后var已经声明了
    intVal,intVal1 :=1,2 //这样就不会错,因为存在有声明新的变量
    

    下面这种方式常用于声明全局变量,全局变量允许声明但不使用。

var(
	vname1 v_type
    vname2 v_type
)

其他注意事项

  • 简单交换两个变量的值,a,b=b,a,注意变量的类型必须是相同的!
  • 空白标识符_可用于抛弃值,比如说_ , b=5,7中,5就是被抛弃的值。 _相当是一个只写变量,得不到它的值,因为Go中必须使用被声明的变量,但有时候不需要从函数中得到返回值。
  • 并行赋值也用于当一个函数返回多个返回值的时候(和python有点类似),var,err=Func1(var1)

附上一个关于空白标识符在函数返回值时的使用case:

package main

import "fmt"

func main(){
    _,num,str=getVal()
    fmt.Println(numb,strs)
}

func getVal()(int,int,string){
    return 1,2,"hi"
}

关于值类型和引用类型

​ 值类型的变量的值放在栈中,int、float、bool、stirng这些基本类型都是值类型,使用这些类型额变量直接指向在内存中的值。

​ 使用=将一个变量的值赋给另一个变量的时候,实际上是在内存中将i的值进行了拷贝。一个引用类型的变量存储的是这个变量的值所在的内存地址,或者内存地址中第一个字所在的位置,内存地址称之为指针。如果是对引用类型用=进行赋值,那么只有引用地址被复制,r2=r1,如果r1的值修改了,那么r2也会受影响。

关于字符串类型

a ="hello"
unsafe.Sizeof(a)

输出结果是16,字符串类型在go中是个结构,包含指向底层数组的指针和长度,二者各是8个字节,所以一共是16.

四、Go 语言常量

​ 常量的定义格式:

const indetifier [type] = value 

​ 如:

const b string = "hello"
const b "hello"

​ 多个相同类型的声明:

const c_name1,c_name2=val1,val2

​ 常量做枚举:

const(
	Unknown = 0
    Female =1
    Male =2
)

​ 常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:

const(
	a="av"
    b=len(a)
    c=unsafe.Sizeof(a)
)

iota

​ 一个可以被编译器修改的常量。

const (
	a=iota
    b
    c
)

​ 第一个iota等于0,每当iota在新的一行被使用的时候,值自动+1,所以b=2,c=3。

package main

import "fmt"

func main(){
    const(
    	a = iota 
        b
        c
        d ="ha"
        e
        f = 100
        g
        h =iota
        i
    )
    fmt.Println(a,b,c,d,e,f,g,h,i)
}

​ 通过运行结果可以看出来:在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。

​ 运行结果:

0 1 2 ha ha 100 100 7 8

​ iota 只是在同一个 const 常量组内递增,每当有新的 const 关键字时,iota 计数会重新开始。

五、Go 语言运算符

​ 内置的运算符有:算数运算符、关系运算符、逻辑运算符、位运算符、赋值运算符、其他运算符。

​ 说下其他运算符:&a给出变量的实际地址,*a表示指针变量。

var a int = 4
var ptr *int

ptr = &a 
fmt.Println("%d",a)
fmt.Println("%d",*ptr)

​ 打印出来的值都是4。

​ 另外,Go的自增自减只能作为表达式来使用,不能用于赋值语句。

a = a++ //在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。

六、Go 语言条件语句

​ Go没有三目运算符,不支持?:形式的条件判断。

  • if
  • if…else
  • if嵌套
  • switch语句
  • select语句

七、Go 语言循环

  • for循环
for init; condition; post { }
for ; condition; {}
for{}
//for-each range 
strings := []string{"google", "runoob"}
for i, s := range strings {
    fmt.Println(i, s)
}


numbers := [6]int{1, 2, 3, 5}
for i,x:= range numbers {
    fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
}  
  • 循环控制语句:break、continue、goto

八、Go 语言函数

func function_name([parameter list] [return_types]){
    
}

​ Go中同时有函数和方法,一个方法就是包含了接受者的函数,接收者可以是命名类型或者结构体类型的一个值或者是一个指针,所有给定类型的方法属于该类型的方法集。

func (variable_name variable_data_type) function_name() [return_type]{
    
}

​ 举个例子:

package main

import "fmt"

type Circle struct{
    redius float64
}

func main(){
    var c Circle
    c.radius=10
    fmt.Println(c.getArea())
    c.changeRadius(20)
    fmt.Println(c.radius)
    change(&c, 30)
    fmt.Println(c.radius)
}

func (c Circle)getArea() float64{
    return 3.14*c.radius*c.radius
}
// 注意如果想要更改成功c的值,这里需要传指针
func (c *Circle) changeRadius(radius float64)  {
   c.radius = radius
}
// 引用类型要想改变值需要传指针
func change(c *Circle, radius float64)  {
   c.radius = radius
}

​ 闭包:

package main
import "fmt"
func main() {
    add_func := add(1,2)
    fmt.Println(add_func(1,1))
    fmt.Println(add_func(0,0))
    fmt.Println(add_func(2,2))
} 
// 闭包使用方法
func add(x1, x2 int) func(x3 int,x4 int)(int,int,int)  {
    i := 0
    return func(x3 int,x4 int) (int,int,int){ 
       i++
       return i,x1+x2,x3+x4
    }
}

九、Go语言变量作用域

  • 函数内定义的变量称为局部变量
  • 函数外定义的变量称为全局变量
  • 函数定义中的变量称为形式参数

十、Go 语言数组

声明数组

var variable_name [SIZE] variable_type
//举例说明
var blance [10] float32

初始化数组

var balance=[5]float32{1000.0,2.0,3.4,7.0,50.0}
var balance=[...]float32{1000.2,5.6}

访问数组元素

var salary float32=balance[9]

以上实例读取了数组balance的第10个元素的值。

多维数组

var threedo, [5][10][4] int

​ 二维数组的定义方式:

var arrayName [x][y] variable_type

初始化二维数组

//Ex1
a=[3][4] int{
    {0,1,2,3},
    {4,5,6,7},
    {8,9,10,11},
}
//Ex2
b=[2][1] int{
    {1,2},{3,4}}

踩坑记录:

  • 把外层括号单独拿到下一次会报错,如果在结尾加上逗号就可以了,比如Ex1
  • 或者如Ex2这样的写法,都在同一行就不需要加逗号

访问二维数组

value :=a[2][3]
var val int = a[2][3]

向函数传递数组

​ 通过形参设定数组大小和形参未设定数组大小两种方式来声明函数。

void func1(param [10]int)
void func2(param []int)

​ 有的博客也说到没有所谓没有声明长度的数组存在

nums :=[3]int{1,2,3,} //声明数组
nums :=[]int{1,2,3} //声明切片

​ 与之前学的C不同的是,Go的数组作为函数参数传递的是副本,函数内修改数组并不改变原来的数组。

package main
import "fmt"
func change(nums[3] int){
   nums[0]=100
}
func main() {
    var nums=[3]int{1,2,3}   
    change(nums)   //nums并未被改变   
    fmt.Println(nums[0])   
    return
}

​ Go的数组是值,长度是其类型的一部分,值传递;Go对数组的处理采用切片的方式,切片包含对底层数组内容的引用,作为函数参数的时候,类似于指针传递

​ 定义了长度的数组只能传递给限制了相同数组长度的函数,没定义长度的只能传递给不限制数组长度的函数。

b:=[]int{1,2,3,4}
func boo(b []int){
    b[0],b[len(b)-1]=b[len(b)-1],b[0]
}
boo(p)
fmt.Println(p) //[4 2 3 1]

p:=[3]int{5,6,7}
func poo(p [3]int){
    p[0],p[len(p)-1]=p[len(p)-1],p[0]
}
poo(p)
fmt.Println(p) //[5,6,7]

十一、Go语言指针

​ 一个指针变量指向了一个值的内存地址,在使用指针之前需要声明指针:

var var_name *var-type

​ 例如:

var ip *int
var fp *float32

使用指针

package main

import "fmt"


func main(){
    
    var a int = 20
    var ip *int
    
    ip=&a
    
    fmt.Println("a的变量地址是:%x\n",&a)
    
    fmt.Println("ip变量存储的指针地址是:%x\n",ip)
    
    fmt.Println("ip变量的值是:%d\n",*ip)
    
}

空指针

​ 一个指针被定义后没有分配到任何变量的时候,值为nil,就是空指针,和其他语言的null、NULL是一样的。

Go指针数组

Go指向指针的指针

Go向函数传递指针参数

十二、Go语言结构体

​ 数组是存储同一类型的数据,但在结构体中可以为不同项定义不同的数据类型。结构体的格式如下:

type struct_variable_type struct{
    member defination
    member defination
    ... ...
    member defination
}

​ 一旦定义结构体类型,只能用于变量的声明,语法格式如下:

variable_name := structure_variable_type{val1,val2...valn}
variable_name := structure_variable_type{key:val1,key2:val2...,keyn:valn}

访问结构体成员

​ 结构体.成员名。

结构体作为函数参数

package main

import "fmt"

type Books struct {
   title string
   author string
   subject string
   book_id int
}

func main() {
   var Book1 Books        /* 声明 Book1 为 Books 类型 */
   var Book2 Books        /* 声明 Book2 为 Books 类型 */

   /* book 1 描述 */
   Book1.title = "Go 语言"
   Book1.author = "www.runoob.com"
   Book1.subject = "Go 语言教程"
   Book1.book_id = 6495407

   /* book 2 描述 */
   Book2.title = "Python 教程"
   Book2.author = "www.runoob.com"
   Book2.subject = "Python 语言教程"
   Book2.book_id = 6495700

   /* 打印 Book1 信息 */
   printBook(Book1)

   /* 打印 Book2 信息 */
   printBook(Book2)
}

func printBook( book Books ) {
   fmt.Printf( "Book title : %s\n", book.title)
   fmt.Printf( "Book author : %s\n", book.author)
   fmt.Printf( "Book subject : %s\n", book.subject)
   fmt.Printf( "Book book_id : %d\n", book.book_id)
}

output:

Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700

结构体指针

​ 结构体是作为参数的值传递,如果想在函数中改变结构体的数据内容,需要传入指针。

package main
import "fmt"

type Books struct{
    title string
    author string
    subject string
    book_id int
}

func main(){
    var Book1 Books
    var Book2 Books
    
   /* book 1 描述 */
   Book1.title = "Go 语言"
   Book1.author = "www.runoob.com"
   Book1.subject = "Go 语言教程"
   Book1.book_id = 6495407

   /* book 2 描述 */
   Book2.title = "Python 教程"
   Book2.author = "www.runoob.com"
   Book2.subject = "Python 语言教程"
   Book2.book_id = 6495700
    
     /* 打印 Book1 信息 */
   printBook(&Book1)

   /* 打印 Book2 信息 */
   printBook(&Book2)
}

func printBook( book *Books ) {
   fmt.Printf( "Book title : %s\n", book.title)
   fmt.Printf( "Book author : %s\n", book.author)
   fmt.Printf( "Book subject : %s\n", book.subject)
   fmt.Printf( "Book book_id : %d\n", book.book_id)
}

output:

Book title : Go 语言
Book author : www.runoob.com
Book subject : Go 语言教程
Book book_id : 6495407
Book title : Python 教程
Book author : www.runoob.com
Book subject : Python 语言教程
Book book_id : 6495700

其他

​ struct 类似于 java 中的类,可以在 struct 中定义成员变量。

type Rect struct{   //定义矩形类
    x,y float64       //类型只包含属性,并没有方法
    width,height float64
}
func (r *Rect) Area() float64{    //为Rect类型绑定Area的方法,*Rect为指针引用可以修改传入参数的值
    return r.width*r.height         //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法
}

十三、Go语言切片

​ 切片是对数组的抽象,Go数组的长度不可改变,在特定场景中就不太灵活,所以提供了内置类型切片,可以理解为是动态数组,长度不固定,可以追加元素,再追加的时候可能使切片的容量增大。

定义切片

var identifier []type
slice := make([]type,len)
//len是数组的长度并且也是切片的初始长度,cap是指定容量
var slice1 []type=make([]type,len[capacity])

切片初始化

//直接初始化切片
s:=[]int {1,2,3}
//初始化切片,是数组arr的引用
s:=arr[:]
//将arr从下标startIndex到endIndex-1 下的元素创建为一个新的切片
s:=arr[startIndex:endIndex]
//类似的有
s := arr[:endIndex] 
s := arr[startIndex:] 
//通过切片s初始化切片s1
s1 := s[startIndex:endIndex] 

len()和cap()函数
切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。切片是可索引的,并且可以由 len() 方法获取长度。切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少。
切片截取
可以通过设置下限及上限来设置截取切片 [lower-bound:upper-bound]
append()和copy()函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。

package main

import "fmt"

func main() {
   var numbers []int
   printSlice(numbers)

   /* 允许追加空切片 */
   numbers = append(numbers, 0)
   printSlice(numbers)

   /* 向切片添加一个元素 */
   numbers = append(numbers, 1)
   printSlice(numbers)

   /* 同时添加多个元素 */
   numbers = append(numbers, 2,3,4)
   printSlice(numbers)

   /* 创建切片 numbers1 是之前切片的两倍容量*/
   numbers1 := make([]int, len(numbers), (cap(numbers))*2)

   /* 拷贝 numbers 的内容到 numbers1 */
   copy(numbers1,numbers)
   printSlice(numbers1)  
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

Output:

len=0 cap=0 slice=[]
len=1 cap=1 slice=[0]
len=2 cap=2 slice=[0 1]
len=5 cap=6 slice=[0 1 2 3 4]
len=5 cap=12 slice=[0 1 2 3 4]

​ 当append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),如果大于那么 cap 不变,否则 cap = 2 * max{cap(list), cap[params]},所以当 append(numbers, 2, 3, 4) cap 从 2 变成 6。

十四、Go语言范围

range关键字用于for循环中迭代数组、切片、通道或集合的元素。在数组和切片中他返回元素的索引和索引对应的值,在集合中返回key-val对。

package main

import "fmt"

func main(){
   
    nums :=[]int{1,2,3}
    sum := 0
    //range返回切片的索引和索引对应的值,如果不需要索引通过_消除掉索引就好了
    for _,num:=range nums{
        sum+=num
    }
    
    fmt.Println("sum:",sum)
    
    kvs := map[string]string{"a":"apple","b":"banana"}
    for k,v :=range kvs {
        fmt.Println("%s -> %s",k,v)
    }
}

​ range还可以用来枚举Unicode字符串,第一个参数是字符的索引, 第二个是字符本身。

for i,c := range "go"{
    fmt.Println(i,c)
}
/*
0 103
1 111
*/

​ 获取参数列表:

package main

import(
	"fmt"
    "os"
)

func main(){
    fmt.Println(len(os.Args))
    for _,arg:=range os.Args{
        fmt.Println(arg)
    }
}

十五、Go语言Map

定义Map

​ 两种方式,可以使用内建函数make也可以使用map关键字来定义Map。

//声明变量,不初始化默认map是nil,nil mao是不能存放键值对的
var map_variable map[key_data_type]value_data_type
//使用make函数
map_variable := make(map[key_data_type]value_data_type)

实例使用:

package main

import "fmt"

func main(){
    
    map_country := make(map[string]string)
    
    map_country["China"]="北京"
    map_country["Japan"]="东京"
    map_country["India"]="新德里"
    
    for country:= range map_country{
        fmt.Println(country,"首都是",map_country[country])
    }
    
    capital,ok=map_country["Italy"]
    if(ok){
        fmt.Println("Italy的首都是:",captital)
    }else{
        fmt.Println("Italy的首都不存在")
    }
}

delete()函数

​ 该函数用于删除集合的元素,参数为map和其对应的key。

delete(map_variable,key_data_variable)

十六、Go 语言递归函数

阶乘问题

package main

import "fmt"

func Factorial(n uint64) uint64{
    if(n>0){
        return n*Factorial(n-1)
    }
    return 1
}

func main(){
    var i int =15
    fmt.Println("%d的阶乘是%d\n",i,Factorial(uint(64)))
}

fibonacci问题

package main

import "fmt"

func fibonacci(n int) int{
    if n<2{
        return n
    }
    return fibonacci(n-1)+fibonacci(n-2)
}

func main(){
    var i int
    for i=0;i<10;i++{
        fmt.Println("%d\t",fibonacci(i))
    }
}

//output:0    1    1    2    3    5    8    13    21    34

十七、Go语言类型转换

type_name(expression)

十八、Go语言接口

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

type interface_name interface(
	method_name1 [return_type]
    method_name2 [return_type]
    ... ...
    method_namen [return_type]
)

/*定义结构体*/
type struct_name struct{
    /*variables*/
}

/*实现接口方法*/
func (struc_name_variable struct_name) method_name1()[return_type]{
    
}
...
func (struct_name_variable struct_name) method_namen()[return_type]

实例:

package main

import (
	"fmt"
)

type phone interface{
    call()
    sendmessage(recv string) string
}

type IPhone struct{
}

func (iPhone IPhone) call(){
    fmt.Println("iPhone call()...")
}

func (iPhone IPhone) sendmessage(recv string) string{
    fmt.Println("sendmessage to",recv)
    return "ok";
}


type HuaWeiPhone struct{
    
}

func (huaWeiPhone HuaWeiPhone) call(){
    fmt.Println("huaWeiPhone call()...")
}

func (huaWeiPhone HuaWeiPhone) sendmessage(recv string) string{
    fmt.Println("sendmessage to",recv)
}


func main(){
    phone:=Phone
    phone = new(IPhone)
    phone.call()
    
    phone=new(HuaWeiPhone)
    phone.call()
    fmt.Println(phone.sendmessage("kilig"))
	
}

注意

  • 如果想要通过接口方法修改属性,需要在传入指针的结构体才行。

    type fruit interface{
        getName() string
        setName(name string)
    }
    
    type apple struct{
        name string
    }
    
    func (a *apple)getName() string{
        return a.name
    }
    
    func (a *apple)setName(name string){
        a.name=name
    }
    
    func main(){
        a:=apple{"红富士"} //a:=apple{name:"红富士"}
        fmt.Println(a.getName())
        a,setName("树顶红")
        fmt.Println(a.getName())
    }
    

十九、Go 错误处理

​ error类型是一个接口类型,这是它的定义:

type error interface{
    Error() string
}

​ 函数通常在最后的返回值中返回错误信息,使用errors.New可返回一个错误信息:

func Sqrt(f float64)(float64,error){
    if f<0{
        return 0,errors.New("math:square root of negative number")
    }
    //implements...
}

​ 如果我们调用Sqrt的时候传入一个负数,那么就会得到non-nil的error对象,将此对象和nil比较,结果是true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:

result,err:=Sqrt(-1)
if err!=nil{
    fmt.Println(err)
}

panic和recover以及defer

​ panic用于主动抛出错误,recover用来捕获panic抛出的错误。

  • 引发panic有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。

  • panic不但可以在函数正常流程中抛出,在defer逻辑里也可以再次调用panic或抛出panic。defer里面的panic能够被后续执行的defer捕获。

  • 发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,一直到被recover捕获或运行到最外层函数。

  • recover用来捕获panic,阻止panic继续向上传递。recover()defer一起使用,但是defer只有在后面的函数体内直接被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。

    多个panic只会捕捉第一个:

package main
import "fmt"
func main(){
    defer func(){
        if err:=recover();err!=nil{
            fmt.Println(err)
        }
    }()
    defer func(){
        panic("three")
    }
    defer func(){
        panic("two")
    }()
    panic("one")
}
//output:three

​ 在网上看到一个例子,关于panicrecoverdefer总结的很好。

package main

import (
"fmt"
)

func main() {
  fmt.Println("外层开始")
  defer func() {
    fmt.Println("外层准备recover")
    if err := recover(); err != nil {
      fmt.Printf("%#v-%#v\n", "外层", err) // err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码
    } else {
      fmt.Println("外层没做啥事")
    }
    fmt.Println("外层完成recover")
  }()
  fmt.Println("外层即将异常")
  f()
  fmt.Println("外层异常后")
  defer func() {
    fmt.Println("外层异常后defer")
  }()
}

func f() {
  fmt.Println("内层开始")
  defer func() {
    fmt.Println("内层recover前的defer")
  }()

  defer func() {
    fmt.Println("内层准备recover")
    if err := recover(); err != nil {
      fmt.Printf("%#v-%#v\n", "内层", err) // 这里err就是panic传入的内容
    }

    fmt.Println("内层完成recover")
  }()

  defer func() {
    fmt.Println("内层异常前recover后的defer")
  }()

  panic("异常信息")

  defer func() {
    fmt.Println("内层异常后的defer")
  }()

  fmt.Println("内层异常后语句") //recover捕获的一级或者完全不捕获这里开始下面代码不会再执行
}

打印结果:

外层开始
外层即将异常
内层开始
内层异常前recover后的defer
内层准备recover
"内层"-"异常信息"
内层完成recover
内层recover前的defer
外层异常后
外层异常后defer
外层准备recover
外层没做啥事
外层完成recover
  • 可以发现位于panic("异常信息")之后的语句就没有再去执行了,然后按照逆序执行defer,并逐级往外层函数栈扩散。
  • 利用recover去捕捉panic的时候,defer需要在panic之前声明,负责由于panic之后的代码得不到执行,因此也无法recover。

二十、Go并发

​ 通过go关键字来开启goroutine即可,goroutine是轻量级线程,其调度是由golang运行时进行管理的。

​ 语法格式:

go function_name(paramlist)
go f(x,y,z)

​ go语句开启一个新的运行期线程,即goroutine,以一个不同的、新创建的goroutine来执行一个函数,同一个程序中的所有goroutine共享一个地址空间。

package main

import(
	"fmt"
    "time"
)

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


func main(){
    go say("world")
    say("hello")
}

​ 输出hello和输出world实际上是两个goroutine在执行。

通道

channel是用来传递数据的一个数据结构,通道可用于两个goroutine之间通过传递一个指定类型的值来同步运行和通讯,操作符<-用于指定通道的方向,发送和或接收。如果未指定,那么是双向通道。

go func(c chan int) { //读写均可的channel c } (a)
go func(c <- chan int) { //只读的Channel } (a)
go func(c chan <- int) {  //只写的Channel } (a)
ch <- v //把v发送到通道ch
v := <-ch //从ch接收数据,并把值赋给v

​ 声明一个通道,使用chan关键字:

ch := make(chan int)

​ 默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。下面这个例子就展示了如何用两个goroutine来计算数字之和,在goroutine完成计算之后,会计算两个结果的和。

package main

import "fmt"

func sum(s []int,c chan int){
	sum:=0
	for _,num:=range s{
		sum+=v
	}
	c<-sum
}


func main(){
	s:=[]int{7, 2, 8, -9, 4, 0}
	
	c:make(chan int)
	
	go sum(s[:len(s)/2],c)
	go sum(s[len(s)/2:],c)
	
	x,y:=<-c,<-c
	
	fmt.Println(x,y,x+y)

}

//output -5 17 12

​ 通道遵循先进先出的原则。

package main

import "fmt"

func main() {
    ch := make(chan int, 2)

    ch <- 1
    a := <-ch
    ch <- 2
    ch <- 3

    fmt.Println(<-ch)
    fmt.Println(<-ch)
    fmt.Println(a)
}
/*
output:
2 
3
1
*/

通道缓冲区

ch :=make(chan int,100)

​ 100就是缓冲区的大小。

​ 带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

​ 不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

**package** main

**import** "fmt"

func main() {
  *// 这里我们定义了一个可以存储整数类型的带缓冲通道*
    *// 缓冲区大小为2*
    ch := make(chan int, 2)

    *// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据*
    *// 而不用立刻需要去同步读取数据*
    ch <- 1
    ch <- 2

   *// 获取这两个数据*
   fmt.Println(<-ch)
   fmt.Println(<-ch)
}	

遍历通道与关闭通道

​ go通过range关键字实现遍历读取到的数据,类似于与数组或切面的样子。关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入。

for i:= range c{
    //...
}
package main

import (
        "fmt"
    	"time"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
		        time.Sleep(1000*time.Millisecond)    
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

han int,100)


​	100就是缓冲区的大小。

​	带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。

​	不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

​	**注意**:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

```go
**package** main

**import** "fmt"

func main() {
  *// 这里我们定义了一个可以存储整数类型的带缓冲通道*
    *// 缓冲区大小为2*
    ch := make(chan int, 2)

    *// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据*
    *// 而不用立刻需要去同步读取数据*
    ch <- 1
    ch <- 2

   *// 获取这两个数据*
   fmt.Println(<-ch)
   fmt.Println(<-ch)
}	

遍历通道与关闭通道

​ go通过range关键字实现遍历读取到的数据,类似于与数组或切面的样子。关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入。

for i:= range c{
    //...
}
package main

import (
        "fmt"
    	"time"
)

func fibonacci(n int, c chan int) {
        x, y := 0, 1
        for i := 0; i < n; i++ {
                c <- x
                x, y = y, x+y
		        time.Sleep(1000*time.Millisecond)    
        }
        close(c)
}

func main() {
        c := make(chan int, 10)
        go fibonacci(cap(c), c)
        // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
        // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
        // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
        // 会结束,从而在接收第 11 个数据的时候就阻塞了。
        for i := range c {
                fmt.Println(i)
        }
}

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值