Go语言-语法基础-全面

一.Go基础

1.1. init函数

go语言中init函数用于包(package)的初始化,该函数是go语言的一个重要特性。

有下面的特征:

  1 init函数是用于程序执行前做包的初始化的函数,比如初始化包里的变量等
 ​
     2 每个包可以拥有多个init函数
 ​
     3 包的每个源文件也可以拥有多个init函数
 ​
     4 同一个包中多个init函数的执行顺序go语言没有明确的定义(说明)
 ​
     5 不同包的init函数按照包导入的依赖关系决定该初始化函数的执行顺序
 ​
     6 init函数不能被其他函数调用,而是在main函数执行之前,自动被调用

1.2. main函数

     Go语言程序的默认入口函数(主函数):func main()
     函数体用{}一对括号包裹。
 ​
     func main(){
         //函数体
     }

1.3. init函数和main函数得异同

 相同点:
         两个函数在定义时不能有任何的参数和返回值,且Go程序自动调用。
     不同点:
         init可以应用于任意包中,且可以重复定义多个。
         main函数只能用于main包中,且只能定义一个。

两个函数得执行顺序

对同一个go文件的init()调用顺序是从上到下的。

对同一个package中不同文件是按文件名字符串比较“从小到大”顺序调用各文件中的init()函数。

对于不同的package,如果不相互依赖的话,按照main包中"先import的后调用"的顺序调用其包中的init(),如果package存在依赖,则先调用最早被依赖的package中的init(),最后调用main函数。

如果init函数中使用了println()或者print()你会发现在执行过程中这两个不会按照你想象中的顺序执行。这两个函数官方只推荐在测试环境中使用,对于正式环境不要使用。

1.4. 下划线

“_”是特殊标识符,用来忽略结果。

1.4.1. 下划线在代码中

 package main
 ​
 import (
     "os"
 )
 ​
 func main() {
     buf := make([]byte, 1024)
     f, _ := os.Open("/Users/***/Desktop/text.txt")
     defer f.Close()
     for {
         n, _ := f.Read(buf)
         if n == 0 {
             break    
 ​
         }
         os.Stdout.Write(buf[:n])
     }
 }

解释:

    //1.
    下划线意思是忽略这个变量.
 ​
     比如os.Open,返回值为*os.File,error
 ​
     普通写法是f,err := os.Open("xxxxxxx")
 ​
     如果此时不需要知道返回的错误值
 ​
     就可以用f, _ := os.Open("xxxxxx")
 ​
     如此则忽略了error变量
     
     //2.
      占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值。
     所以就把该值赋给下划线,意思是丢掉不要。
     这样编译器可以更好的优化,任何类型的单个值都可以丢给下划线。
     这种情况是占位用的,方法返回两个结果,而你只想要一个结果。
     那另一个就用 "_" 占位,而如果用变量的话,不使用,编译器是会报错的。

1.5. 变量和常量

1.5.1. 变量声明

Go语言中的变量需要声明后才能使用,同一作用域内不支持重复声明。并且Go语言的变量声明后必须使用。

1.5.2. 标准声明

Go语言的变量声明格式为:

     var 变量名 变量类型

变量声明以关键字var开头,变量类型放在变量的后面,行尾无需分号。 举个例子:

     var name string
     var age int
     var isOk bool

1.5.3. 批量声明

     var (
         a string
         b int
         c bool
         d float32
     )

1.5.4. 变量得初始化

整型和浮点型变量的默认值为0。 字符串变量的默认值为空字符串。 布尔型变量默认为false。 切片、函数、指针变量的默认为nil

     var 变量名 类型 = 表达式
     var name string = "Alex"
     var age int = 18
     //一次初始化多个变量
     var name, age = "brooks", 18

短变量的声明

     func main() {
         n := 10
         m := 100
     }

匿名变量的声明

 func foo() (int, string) {
     return 10, "Q1mi"
 }
 func main() {
     x, _ := foo()
     _, y := foo()
     fmt.Println("x=", x)
     fmt.Println("y=", y)
 }
 //匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。 (在Lua等编程语言里,匿名变量也被叫做哑元变量。)

注意:

   函数外的每个语句都必须以关键字开始(var、const、func等)
 ​
     :=不能使用在函数外。
 ​
     _多用于占位,表示忽略值。

1.5.5. 常量

常量的声明

   const pi = 3.1415   //常量必须初始化赋值
   const e = 2.7182
     
   const (
      pi = 3.1415
      e = 2.7182
   )
     
   const (
      n1 = 100
      n2
      n3
  )

1.5.6. iota

    const (
             n1 = iota //0
             n2        //1
             n3        //2
             n4        //3
         )
 //iota是go语言的常量计数器,只能在常量的表达式中使用。 iota在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota计数一次(iota可理解为const语句块中的行索引)。 使用iota能简化定义,在定义枚举时很有用。

1.6. 基本类型

1.6.1. 整型

整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64

其中,uint8就是我们熟知的byte型,int16对应C语言中的short型,int64对应C语言中的long型。

1.6.2. 浮点型

Go语言支持两种浮点型数:float32float64

1.6.3. 复数

complex64complex128

复数有实部和虚部,complex64的实部和虚部为32位,complex128的实部和虚部为64位。

1.6.4. 布尔值

    Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。
    
    布尔类型变量的默认值为false。

    Go 语言中不允许将整型强制转换为布尔型.

    布尔型无法参与数值运算,也无法与其他类型进行转换。

1.6.5. 字符串

1.6.6. 字符串转义

1.6.7. 字符串常用操作

方法 介绍
len(str) 求长度
+或fmt.Sprintf 拼接字符串
strings.Split 分割
strings.Contains 判断是否包含
strings.HasPrefix,strings.HasSuffix 前缀/后缀判断
strings.Index(),strings.LastIndex() 子串出现的位置
strings.Join(a[]string, sep string) join操作

1.7. 数组Array

Golang Array和以往认知的数组有很大不同。

    1. 数组:是同一种数据类型的固定长度的序列。
    2. 数组定义:var a [len]int,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。
    3. 长度是数组类型的一部分,因此,var a[5] int和var a[10]int是不同的类型。
    4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
    5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic
    6. 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
    7.支持 "=="、"!=" 操作符,因为内存总是被初始化过的。
    8.指针数组 [n]*T,数组指针 *[n]T。

1.7.1. 数组的初始化

package main

import (
    "fmt"
)

var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}

func main() {
    a := [3]int{1, 2}           // 未初始化元素值为 0。
    b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
    c := [5]int{2: 100, 4: 200} // 使用引号初始化元素。
    d := [...]struct {
        name string
        age  uint8
    }{
        {"user1", 10}, // 可省略元素类型。
        {"user2", 20}, // 别忘了最后一行的逗号。
    }
    fmt.Println(arr0, arr1, arr2, str)
    fmt.Println(a, b, c, d)
}

多维数组

package main

import (
    "fmt"
)

var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}

func main() {
    a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
    b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。
    fmt.Println(arr0, arr1)
    fmt.Println(a, b)
}

多维数组的遍历

 package main
 ​
 import (
     "fmt"
 )
 ​
 func main() {
 ​
     var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
 ​
     for k1, v1 := range f {
         for k2, v2 := range v1 {
             fmt.Printf("(%d,%d)=%d ", k1, k2, v2)
         }
         fmt.Println()
     }
 }

1.8. 切片Slice

需要说明,slice 并不是数组或数组指针。它通过内部指针和相关属性引用数组片段,以实现变长方案。

     1. 切片:切片是数组的一个引用,因此切片是引用类型。但自身是结构体,值拷贝传递。
     2. 切片的长度可以改变,因此,切片是一个可变的数组。
     3. 切片遍历方式和数组一样,可以用len()求长度。表示可用元素数量,读写操作不能超过该限制。 
     4. cap可以求出slice最大扩张容量,不能超出数组限制。0 <= len(slice) <= len(array),其中array是slice引用的数组。
     5. 切片的定义:var 变量名 []类型,比如 var str []string  var arr []int。
     6. 如果 slice == nil,那么 len、cap 结果都等于 0。

1.8.1. 创建切片的各种方式

 
package main
 ​
 import "fmt"
 ​
 func main() {
    //1.声明切片
    var s1 []int
    if s1 == nil {
       fmt.Println("是空")
    } else {
       fmt.Println("不是空")
    }
    // 2.:=
    s2 := []int{}
    // 3.make()
    var s3 []int = make([]int, 0)
    fmt.Println(s1, s2, s3)
    // 4.初始化赋值
    var s4 []int = make([]int, 0, 0)
    fmt.Println(s4)
    s5 := []int{1, 2, 3}
    fmt.Println(s5)
    // 5.从数组切片
    arr := [5]int{1, 2, 3, 4, 5}
    var s6 []int
    // 前包后不包
    s6 = arr[1:4]
    fmt.Println(s6)
 }

Go语言-语法基础-全面_第1张图片

1.8.2. 通过make来创建切片

  var slice []type = make([]type, len)
    slice  := make([]type, len)
    slice  := make([]type, len, cap)

1.8.3. 用append内置函数操作切片(切片追加)

package main

import (
    "fmt"
)

func main() {

    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a)
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b)
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c)
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d)
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e)

}

1.8.4. slice遍历

package main

import (
    "fmt"
)

func main() {

    data := [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    slice := data[:]
    for index, value := range slice {
        fmt.Printf("inde : %v , value : %v\n", index, value)
    }

}

切片冒号的理解

golang slice data[:6:8] 两个冒号的理解

常规slice , data[6:8],从第6位到第8位(返回6, 7),长度len为2, 最大可扩充长度cap为4(6-9)

另一种写法: data[:6:8] 每个数字前都有个冒号, slice内容为data从0到第6位,长度len为6,最大扩充项cap设置为8

a[x:y:z] 切片内容 [x:y] 切片长度: y-x 切片容量:z-x

1.9. 指针

只需要知道三个概念: 指针地址, 指针类型, 指针取值

1.9.1. Go语言中的指针

Go语言中的函数传参都是值拷贝,当我们想要修改某个变量的时候,我们可以创建一个指向该变量地址的指针变量。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。Go语言中的指针操作非常简单,只需要记住两个符号:&(取地址)和*(根据地址取值)。

1.9.2. 指针地址和指针类型

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int、*int64、*string等。

取变量指针的语法如下:

    ptr := &v    // v的类型为T

其中:

    v:代表被取地址的变量,类型为T
    ptr:用于接收地址的变量,ptr的类型就为*T,称做T的指针类型。*代表指针。

举个例子:

func main() {
    a := 10
    b := &a
    fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
    fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
    fmt.Println(&b)                    // 0xc00000e018
}

Go语言-语法基础-全面_第2张图片

1.9.3. 指针取值

func main() {
    //指针取值
    a := 10
    b := &a // 取变量a的地址,将指针保存到b中
    fmt.Printf("type of b:%T\n", b)
    c := *b // 指针取值(根据指针去内存取值)
    fmt.Printf("type of c:%T\n", c)
    fmt.Printf("value of c:%v\n", c)
}

//总结: 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

指针传值得例子:

func modify1(x int) {
    x = 100
}

func modify2(x *int) {
    *x = 100
}

func main() {
    a := 10
    modify1(a)
    fmt.Println(a) // 10
    modify2(&a)
    fmt.Println(a) // 100
}

1.9.4. 空指针

  • 当一个指针被定义后没有分配到任何变量时,它的值为 nil

  • 空指针的判断

 package main
 ​
 import "fmt"
 ​
 func main() {
     var p *string
     fmt.Println(p)
     fmt.Printf("p的值是%v\n", p)
     if p != nil {
         fmt.Println("非空")
     } else {
         fmt.Println("空值")
     }
 }

1.9.5. new和make得区别

     1.二者都是用来做内存分配的。
     2.make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
     3.而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。

1.10. Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型,必须初始化才能使用。

1.10.1. map的定义

Go语言中 map的定义语法如下

     map[KeyType]ValueType

其中,

     KeyType:表示键的类型。
 ​
     ValueType:表示键对应的值的类型。

map类型的变量默认初始值为nil,需要使用make()函数来分配内存。语法为:

    make(map[KeyType]ValueType, [cap])

其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量

1.10.2. map基本的使用

map中的数据都是成对出现的,map的基本使用示例代码如下:

func main() {
    scoreMap := make(map[string]int, 8)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    fmt.Println(scoreMap)
    fmt.Println(scoreMap["小明"])
    fmt.Printf("type of a:%T\n", scoreMap)
}

//输出
	/*map[小明:100 张三:90]
    100
    type of a:map[string]int*/

1.10.3. 判断某个键是否存在

 value, ok := map[key]
func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    // 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值
    v, ok := scoreMap["张三"]
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("查无此人")
    }
}

1.10.4. map的遍历

Go语言中使用for range遍历map。

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k, v := range scoreMap {
        fmt.Println(k, v)
    }
}

但我们只想遍历key的时候,可以按下面的写法:

func main() {
    scoreMap := make(map[string]int)
    scoreMap["张三"] = 90
    scoreMap["小明"] = 100
    scoreMap["王五"] = 60
    for k := range scoreMap {
        fmt.Println(k)
    }
}

注意: 遍历map时的元素顺序与添加键值对的顺序无关。

1.11. 结构体

Go语言中没有“类”的概念,也不支持“类”的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。

1.11.1. 类型别名和自定义类型

自定义类型

在Go语言中有一些基本的数据类型,如string、整型、浮点型、布尔等数据类型,Go语言中可以使用type关键字来定义自定义类型。

   //将MyInt定义为int类型
    type MyInt int
   //通过Type关键字的定义,MyInt就是一种新的类型,它具有int的特性。

类型别名

类型别名是Go1.9版本添加的新功能。

类型别名规定:TypeAlias只是Type的别名,本质上TypeAlias与Type是同一个类型。就像一个孩子小时候有小名、乳名,上学后用学名,英语老师又会给他起英文名,但这些名字都指的是他本人。

    type TypeAlias = Type

我们之前见过的rune和byte就是类型别名,他们的定义如下:

    type byte = uint8
    type rune = int32

1.11.2. 类型定义和类型别名的区别

//类型定义
type NewInt int

//类型别名
type MyInt = int

func main() {
    var a NewInt
    var b MyInt

    fmt.Printf("type of a:%T\n", a) //type of a:main.NewInt
    fmt.Printf("type of b:%T\n", b) //type of b:int
}
/*
结果显示a的类型是main.NewInt,表示main包下定义的NewInt类型。b的类型是int。MyInt类型只会在代码中存在,编译完成时并不会有MyInt类型。
*/

1.11.3. 结构体得定义

使用type和struct关键字来定义结构体,具体代码格式如下:

    type 类型名 struct {
        字段名 字段类型
        字段名 字段类型
        …
    }

其中:

    1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
    2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
    3.字段类型:表示结构体字段的具体类型。

举个例子,我们定义一个Person(人)结构体,代码如下:

     type person struct {
         name string
         city string
         age  int8
     }

同样类型的字段也可以写在一行,

     type person1 struct {
         name, city string
         age        int8
     }

1.11.4. 基本实例化

 type person struct {
     name string
     city string
     age  int8
 }
 ​
 func main() {
     var p1 person
     p1.name = "pprof.cn"
     p1.city = "北京"
     p1.age = 18
     fmt.Printf("p1=%v\n", p1)  //p1={pprof.cn 北京 18}
     fmt.Printf("p1=%#v\n", p1) //p1=main.person{name:"pprof.cn", city:"北京", age:18}
 }
 //我们通过.来访问结构体的字段(成员变量),例如p1.name和p1.age等。

1.11.5. 匿名结构体

 package main
 ​
 import (
     "fmt"
 )
 ​
 func main() {
     var user struct{Name string; Age int}
     user.Name = "pprof.cn"
     user.Age = 18
     fmt.Printf("%#v\n", user)
 }

1.11.6. 创建指针类型结构体

我们还可以通过使用new关键字对结构体进行实例化,得到的是结构体的地址。 格式如下:

     var p2 = new(person)
     fmt.Printf("%T\n", p2)     //*main.person
     fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"", city:"", age:0}

从打印的结果中我们可以看出p2是一个结构体指针。

需要注意的是在Go语言中支持对结构体指针直接使用.来访问结构体的成员。

     var p2 = new(person)
     p2.name = "测试"
     p2.age = 18
     p2.city = "北京"
     fmt.Printf("p2=%#v\n", p2) //p2=&main.person{name:"测试", city:"北京", age:18}

1.11.7. 取结构体的地址实例化

使用&对结构体进行取地址操作相当于对该结构体类型进行了一次new实例化操作。

p3 := &person{}
fmt.Printf("%T\n", p3)     //*main.person
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"", city:"", age:0}
p3.name = "博客"
p3.age = 30
p3.city = "成都"
fmt.Printf("p3=%#v\n", p3) //p3=&main.person{name:"博客", city:"成都", age:30}

p3.name = "博客"其实在底层是(*p3).name = "博客",这是Go语言帮我们实现的语法糖。

1.11.8. 结构体初始化

type person struct {
    name string
    city string
    age  int8
}

func main() {
    var p4 person
    fmt.Printf("p4=%#v\n", p4) //p4=main.person{name:"", city:"", age:0}
}

1.11.9. 使用键值对初始化

使用键值对对结构体进行初始化时,键对应结构体的字段,值对应该字段的初始值。

p5 := person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p5=%#v\n", p5) //p5=main.person{name:"pprof.cn", city:"北京", age:18}

也可以对结构体指针进行键值对初始化,例如:

p6 := &person{
    name: "pprof.cn",
    city: "北京",
    age:  18,
}
fmt.Printf("p6=%#v\n", p6) //p6=&main.person{name:"pprof.cn", city:"北京", age:18}

当某些字段没有初始值的时候,该字段可以不写。此时,没有指定初始值的字段的值就是该字段类型的零值。

p7 := &person{
    city: "北京",
}
fmt.Printf("p7=%#v\n", p7) //p7=&main.person{name:"", city:"北京", age:0}

1.11.10. 结构体内存布局

type test struct {
    a int8
    b int8
    c int8
    d int8
}
n := test{
    1, 2, 3, 4,
}
fmt.Printf("n.a %p\n", &n.a)
fmt.Printf("n.b %p\n", &n.b)
fmt.Printf("n.c %p\n", &n.c)
fmt.Printf("n.d %p\n", &n.d)
/* 输出结果
	n.a 0xc0000a0060
    n.b 0xc0000a0061
    n.c 0xc0000a0062
    n.d 0xc0000a0063
*/

1.11.11. 构造函数

Go语言的结构体没有构造函数,我们可以自己实现。 例如,下方的代码就实现了一个person的构造函数。 因为struct是值类型,如果结构体比较复杂的话,值拷贝性能开销会比较大,所以该构造函数返回的是结构体指针类型。

func newPerson(name, city string, age int8) *person {
    return &person{
        name: name,
        city: city,
        age:  age,
    }
}

调用构造函数

p9 := newPerson("pprof.cn", "测试", 90)
fmt.Printf("%#v\n", p9)

二. 流程控制

2.1. 条件语句if

2.1.1. 基本语法

Go 编程语言中 if 语句的语法如下:

    • 可省略条件表达式括号。
    • 持初始化语句,可定义代码块局部变量。 
    • 代码块左 括号必须在条件表达式尾部。

    if 布尔表达式 {
    /* 在布尔表达式为 true 时执行 */
    }

2.2. 条件语句switch

2.2.1. Type Swich

switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。

switch x.(type){
    case type:
       statement(s)      
    case type:
       statement(s)
    /* 你可以定义任意个数的case */
    default: /* 可选 */
       statement(s)
}

实例:

package main

import "fmt"

func main() {
    var x interface{}
    //写法一:
    switch i := x.(type) { // 带初始化语句
    case nil:
        fmt.Printf(" x 的类型 :%T\r\n", i)
    case int:
        fmt.Printf("x 是 int 型")
    case float64:
        fmt.Printf("x 是 float64 型")
    case func(int) float64:
        fmt.Printf("x 是 func(int) 型")
    case bool, string:
        fmt.Printf("x 是 bool 或 string 型")
    default:
        fmt.Printf("未知型")
    }
    //写法二
    var j = 0
    switch j {
    case 0:
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var k = 0
    switch k {
    case 0:
        println("fallthrough")
        fallthrough
        /*
            Go的switch非常灵活,表达式不必是常量或整数,执行的过程从上至下,直到找到匹配项;
            而如果switch没有表达式,它会匹配true。
            Go里面switch默认相当于每个case最后带有break,
            匹配成功后不会自动向下执行其他case,而是跳出整个switch,
            但是可以使用fallthrough强制执行后面的case代码。
        */
    case 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法三
    var m = 0
    switch m {
    case 0, 1:
        fmt.Println("1")
    case 2:
        fmt.Println("2")
    default:
        fmt.Println("def")
    }
    //写法四
    var n = 0
    switch { //省略条件表达式,可当 if...else if...else
    case n > 0 && n < 10:
        fmt.Println("i > 0 and i < 10")
    case n > 10 && n < 20:
        fmt.Println("i > 10 and i < 20")
    default:
        fmt.Println("def")
    }
}

2.3. 条件语句select

2.3.1. select语句

select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select 是Go中的一个控制结构,类似于用于通信的switch语句。每个case必须是一个通信操作,要么是发送要么是接收。 select 随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。一个默认的子句应该总是可运行的。

 select {
     case communication clause  :
        statement(s);      
     case communication clause  :
        statement(s);
     /* 你可以定义任意数量的 case */
     default : /* 可选 */
        statement(s);
 }
 ​
 /*
     每个case都必须是一个通信
     所有channel表达式都会被求值
     所有被发送的表达式都会被求值
     如果任意某个通信可以进行,它就执行;其他被忽略。
     如果有多个case都可以运行,Select会随机公平地选出一个执行。其他不会执行。
     否则:
     如果有default子句,则执行该语句。
     如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。
 */

2.4. 循环语句 for

语法:

     for init; condition; post { }
     for condition { }
     for { }
     init: 一般为赋值表达式,给控制变量赋初值;
     condition: 关系表达式或逻辑表达式,循环控制条件;
     post: 一般为赋值表达式,给控制变量增量或减量。
     for语句执行过程如下:
     ①先对表达式 init 赋初值;
     ②判别赋值表达式 init 是否满足给定 condition 条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句。

实例:

 package main
 ​
 import "fmt"
 ​
 func main() {
 ​
    var b int = 15
    var a int
 ​
    numbers := [6]int{1, 2, 3, 5}
 ​
    /* for 循环 */
    for a := 0; a < 10; a++ {
       fmt.Printf("a 的值为: %d\n", a)
    }
 ​
    for a < b {
       a++
       fmt.Printf("a 的值为: %d\n", a)
       }
 ​
    for i,x:= range numbers {
       fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
    }   
 }

2.5. 循环语句range

Golang range类似迭代器操作,返回 (索引, 值) 或 (键, 值)。

for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:

for key, value := range oldMap {
    newMap[key] = value
}
1st value 2nd value
string index s[index] unicode, rune
array/slice index s[index]
map key m[key]
channel element

可忽略不想要的返回值,或 "_" 这个特殊变量。

package main

func main() {
    s := "abc"
    // 忽略 2nd value,支持 string/array/slice/map。
    for i := range s {
        println(s[i])
    }
    // 忽略 index。
    for _, c := range s {
        println(c)
    }
    // 忽略全部返回值,仅迭代。
    for range s {

    }

    m := map[string]int{"a": 1, "b": 2}
    // 返回 (key, value)。
    for k, v := range m {
        println(k, v)
    }
}

*注意,range 会复制对象。

另外两种引用类型 map、channel 是指针包装,而不像 slice 是 struct。

for 和 for range有什么区别?

主要是使用场景不同

for可以

遍历array和slice

遍历key为整型递增的map

遍历string

for range可以完成所有for可以做的事情,却能做到for不能做的,包括

遍历key为string类型的map并同时获取key和value

遍历channel

2.6. 循环控制Goto, Break, Continue

循环控制语句

循环控制语句可以控制循环体内语句的执行过程。

GO 语言支持以下几种循环控制语句:

2.6.1. Goto、Break、Continue

     1.三个语句都可以配合标签(label)使用
    2.标签名区分大小写,定以后若不使用会造成编译错误
    3.continue、break配合标签(label)可用于多层循环跳出
    4.goto是调整执行位置,与continue、break配合标签(label)的结果并不相同

三. 函数

3.1. 函数的定义

golang得函数特点:

	• 无需声明原型。
    • 支持不定 变参。
    • 支持多返回值。
    • 支持命名返回参数。 
    • 支持匿名函数和闭包。
    • 函数也是一种类型,一个函数可以赋值给变量。

    • 不支持 嵌套 (nested) 一个包不能有两个名字一样的函数。
    • 不支持 重载 (overload) 
    • 不支持 默认参数 (default parameter)。

3.1.1. 函数声明

函数声明包含一个函数名,参数列表, 返回值列表和函数体。如果函数没有返回值,则返回列表可以省略。函数从第一条语句开始执行,直到执行return语句或者执行函数的最后一条语句。

函数可以没有参数或接受多个参数。

注意类型在变量名之后 。

当两个或多个连续的函数命名参数是同一类型,则除了最后一个类型之外,其他都可以省略。

函数可以返回任意数量的返回值。

使用关键字 func 定义函数,左大括号依旧不能另起一行。

func test(x, y int, s string) (int, string) {
    // 类型相同的相邻参数,参数类型可合并。 多返回值必须用括号。
    n := x + y          
    return n, fmt.Sprintf(s, n)
}

函数是第一类对象,可作为参数传递。建议将复杂签名定义为函数类型,以便于阅读。

 package main
 ​
 import "fmt"
 ​
 func test(fn func() int) int {
     return fn()
 }
 // 定义函数类型。
 type FormatFunc func(s string, x, y int) string 
 ​
 func format(fn FormatFunc, s string, x, y int) string {
     return fn(s, x, y)
 }
 ​
 func main() {
     s1 := test(func() int { return 100 }) // 直接将匿名函数当参数。
 ​
     s2 := format(func(s string, x, y int) string {
         return fmt.Sprintf(s, x, y)
     }, "%d, %d", 10, 20)
 ​
     println(s1, s2)
 }

3.2. 参数

   func myfunc(args ...int) {    //0个或多个参数
   }
 ​
   func add(a int, args…int) int {    //1个或多个参数
   }
 ​
   func add(a int, b int, args…int) int {    //2个或多个参数
   }
   //注意:其中args是一个slice,我们可以通过arg[index]依次访问所有参数,通过len(arg)来判断传递参数的个数.

用interface{}传递任意类型数据是Go语言的惯例用法,而且interface{}是类型安全的。

 func myfunc(args ...interface{}) {
   }

代码:

 package main
 ​
 import (
     "fmt"
 )
 ​
 func test(s string, n ...int) string {
     var x int
     for _, i := range n {
         x += i
     }
 ​
     return fmt.Sprintf(s, x)
 }
 ​
 func main() {
     println(test("sum: %d", 1, 2, 3))
 }

使用 slice 对象做变参时,必须展开。(slice...)

package main

import (
    "fmt"
)

func test(s string, n ...int) string {
    var x int
    for _, i := range n {
        x += i
    }

    return fmt.Sprintf(s, x)
}

func main() {
    s := []int{1, 2, 3}
    res := test("sum: %d", s...)    // slice... 展开slice
    println(res)
}

3.3. 返回值

3.3.1. 函数返回值

"_"标识符,用来忽略函数的某个返回值

Go 的返回值可以被命名,并且就像在函数体开头声明的变量那样使用。

返回值的名称应当具有一定的意义,可以作为文档使用。

没有参数的 return 语句返回各个返回变量的当前值。这种用法被称作“裸”返回。

直接返回语句仅应当用在像下面这样的短函数中。在长的函数中它们会影响代码的可读性。

package main

import (
    "fmt"
)

func add(a, b int) (c int) {
    c = a + b
    return
}

func calc(a, b int) (sum int, avg int) {
    sum = a + b
    avg = (a + b) / 2

    return
}

func main() {
    var a, b int = 1, 2
    c := add(a, b)
    sum, avg := calc(a, b)
    fmt.Println(a, b, c, sum, avg)
}
//输出结果:   1 2 3 3 1

Golang返回值不能用容器对象接收多返回值。只能用多个变量,或 "_" 忽略。

package main

func test() (int, int) {
    return 1, 2
}

func main() {
    // s := make([]int, 2)
    // s = test()   // Error: multiple-value test() in single-value context

    x, _ := test()
    println(x)
}
//输出结果 1

3.4. 匿名函数

在Go里面,函数可以像普通变量一样被传递或使用,Go语言支持随时在代码里定义匿名函数。

匿名函数由一个不带函数名的函数声明和函数体组成。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

package main

import (
    "fmt"
    "math"
)

func main() {
    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4))
}
//输出结果 2

Golang匿名函数可赋值给变量,做为结构字段,或者在 channel 里传送。

 
package main
 ​
 func main() {
     // --- function variable ---
     fn := func() { println("Hello, World!") }
     fn()
 ​
     // --- function collection ---
     fns := [](func(x int) int){
         func(x int) int { return x + 1 },
         func(x int) int { return x + 2 },
     }
     println(fns[0](100))
 ​
     // --- function as field ---
     d := struct {
         fn func() string
     }{
         fn: func() string { return "Hello, World!" },
     }
     println(d.fn())
 ​
     // --- channel of function ---
     fc := make(chan func() string, 2)
     fc <- func() string { return "Hello, World!" }
     println((<-fc)())
 }
 //输出结果
     Hello, World!
     101
     Hello, World!
     Hello, World!

3.5. 闭包, 递归

Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。 下面我来将之前的JavaScript的闭包例子用Go来实现。

 package main
 ​
 import (
     "fmt"
 )
 ​
 func a() func() int {
     i := 0
     b := func() int {
         i++
         fmt.Println(i)
         return i
     }
     return b
 }
 ​
 func main() {
     c := a()
     c()
     c()
     c()
 ​
     a() //不会输出i
 }
 //输出结果 1  2  3

3.5.1. 递归

递归,就是在运行的过程中调用自己。 一个函数调用自己,就叫做递归函数。

构成递归需具备的条件:

    1.子问题须与原始问题为同样的事,且更为简单。
    2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。

数字阶乘例子:

package main

import "fmt"

func factorial(i int) int {
    if i <= 1 {
        return 1
    }
    return i * factorial(i-1)
}

func main() {
    var i int = 7
    fmt.Printf("Factorial of %d is %d\n", i, factorial(i))
}

3.6. 延迟调用(defer)

3.6.1. defer特性:

    1. 关键字 defer 用于注册延迟调用。
    2. 这些调用直到 return 前才被执。因此,可以用来做资源清理。
    3. 多个defer语句,按先进后出的方式执行。
    4. defer语句中的变量,在defer声明时就决定了。

3.6.2. defer用途

    1. 关闭文件句柄
    2. 锁资源释放
    3. 数据库连接释放

go语言 defer

go 语言的defer功能强大,对于资源管理非常方便,但是如果没用好,也会有陷阱。

defer 是先进后出

这个很自然,后面的语句会依赖前面的资源,因此如果先前面的资源先释放了,后面的语句就没法执行了。

package main

import "fmt"

func main() {
    var whatever [5]struct{}

    for i := range whatever {
        defer fmt.Println(i)
    }
}
//输出结果 4 3 2 1 0
3.6.3. defer碰上闭包

 package main
 ​
 import "fmt"
 ​
 func main() {
     var whatever [5]struct{}
     for i := range whatever {
         defer func() { fmt.Println(i) }()
     }
 }
 //输出结果 4 4 4 4 4 4 
 /*
 其实go说的很清楚,我们一起来看看go spec如何说的
 ​
 Each time a "defer" statement executes, the function value and parameters to the call are evaluated as usualand saved anew but the actual function is not invoked.
 ​
 也就是说函数正常执行,由于闭包用到的变量 i 在执行的时候已经变成4,所以输出全都是4.
 */

3.7. 异常处理

Golang 没有结构化异常,使用 panic 抛出错误,recover 捕获错误。

异常的使用场景简单描述:Go中可以抛出一个panic的异常,然后在defer中通过recover捕获这个异常,然后正常处理。

panic:

     1、内置函数
     2、假如函数F中书写了panic语句,会终止其后要执行的代码,在panic所在函数F内如果存在要执行的defer函数列表,按照defer的逆序执行
     3、返回函数F的调用者G,在G中,调用函数F语句之后的代码不会执行,假如函数G中存在要执行的defer函数列表,按照defer的逆序执行
     4、直到goroutine整个退出,并报告错误

recover:

     1、内置函数
     2、用来控制一个goroutine的panicking行为,捕获panic,从而影响应用的行为
     3、一般的调用建议
         a). 在defer函数中,通过recever来终止一个goroutine的panicking过程,从而恢复正常代码的执行
         b). 可以获取通过panic传递的error

注意:

     1.利用recover处理panic指令,defer 必须放在 panic 之前定义,另外 recover 只有在 defer 调用的函数中才有效。否则当panic时,recover无法捕获到panic,无法防止panic扩散。
     2.recover 处理异常后,逻辑并不会恢复到 panic 那个点去,函数跑到 defer 之后的那个点。
     3.多个 defer 会形成 defer 栈,后定义的 defer 语句会被最先调用。
package main

func main() {
    test()
}

func test() {
    defer func() {
        if err := recover(); err != nil {
            println(err.(string)) // 将 interface{} 转型为具体类型。
        }
    }()

    panic("panic error!")
}
//输出结果: panic error!

向已关闭的通道发送数据会引发panic

package main

import (
    "fmt"
)

func main() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println(err)
        }
    }()

    var ch chan int = make(chan int, 10)
    close(ch)
    ch <- 1
}
//输出结果 send on closed channel

延迟调用中引发的错误,可被后续延迟调用捕获,但仅最后一个错误可被捕获。

package main

import "fmt"

func test() {
    defer func() {
        fmt.Println(recover())
    }()

    defer func() {
        panic("defer panic")
    }()

    panic("test panic")
}

func main() {
    test()
}
//输出 defer panic

四. 方法

4.1. 方法定义

Golang 方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver)。

• 只能为当前包内命名类型定义方法。
• 参数 receiver 可任意命名。如方法中未曾使用 ,可省略参数名。
• 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。 
• 不支持方法重载,receiver 只是参数签名的组成部分。
• 可用实例 value 或 pointer 调用全部方法,编译器自动转换。

一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。

所有给定类型的方法属于该类型的方法集。

4.1.1. 方法定义

  func (recevier type) methodName(参数列表)(返回值列表){}

    参数和返回值可以省略
 package main
 ​
 type Test struct{}
 ​
 // 无参数、无返回值
 func (t Test) method0() {
 ​
 }
 ​
 // 单参数、无返回值
 func (t Test) method1(i int) {
 ​
 }
 ​
 // 多参数、无返回值
 func (t Test) method2(x, y int) {
 ​
 }
 ​
 // 无参数、单返回值
 func (t Test) method3() (i int) {
     return
 }
 ​
 // 多参数、多返回值
 func (t Test) method4(x, y int) (z int, err error) {
     return
 }
 ​
 // 无参数、无返回值
 func (t *Test) method5() {
 ​
 }
 ​
 // 单参数、无返回值
 func (t *Test) method6(i int) {
 ​
 }
 ​
 // 多参数、无返回值
 func (t *Test) method7(x, y int) {
 ​
 }
 ​
 // 无参数、单返回值
 func (t *Test) method8() (i int) {
     return
 }
 ​
 // 多参数、多返回值
 func (t *Test) method9(x, y int) (z int, err error) {
     return
 }
 ​
 func main() {}

下面定义一个结构体类型和该类型的一个方法:

package main
 ​
 import (
     "fmt"
 )
 ​
 //结构体
 type User struct {
     Name  string
     Email string
 }
 ​
 //方法
 func (u User) Notify() {
     fmt.Printf("%v : %v \n", u.Name, u.Email)
 }
 func main() {
     // 值类型调用方法
     u1 := User{"golang", "[email protected]"}
     u1.Notify()
     // 指针类型调用方法
     u2 := User{"go", "[email protected]"}
     u3 := &u2
     u3.Notify()
 }
 //输出结果: 
 /*
     golang : [email protected] 
     go : [email protected]
 */
 解释:
 /*
 解释: 首先我们定义了一个叫做 User 的结构体类型,然后定义了一个该类型的方法叫做 Notify,该方法的接受者是一个 User 类型的值。要调用 Notify 方法我们需要一个 User 类型的值或者指针。
 ​
 在这个例子中当我们使用指针时,Go 调整和解引用指针使得调用可以被执行。注意,当接受者不是一个指针时,该方法操作对应接受者的值的副本(意思就是即使你使用了指针调用函数,但是函数的接受者是值类型,所以函数内部操作还是对副本的操作,而不是指针操作。
 */

修改Notify方法, 让它得接收者使用指针类型

 package main
 ​
 import (
     "fmt"
 )
 ​
 //结构体
 type User struct {
     Name  string
     Email string
 }
 ​
 //方法
 func (u *User) Notify() {
     fmt.Printf("%v : %v \n", u.Name, u.Email)
 }
 func main() {
     // 值类型调用方法
     u1 := User{"golang", "[email protected]"}
     u1.Notify()
     // 指针类型调用方法
     u2 := User{"go", "[email protected]"}
     u3 := &u2
     u3.Notify()
 }
 //输出结果:
 /*
     golang : [email protected] 
     go : [email protected]
     注意:当接受者是指针时,即使用值类型调用那么函数内部也是对指针的操作。
 */

4.2. 普通函数与方法得区别

1.对于普通函数,接收者为值类型时,不能将指针类型的数据直接传递,反之亦然。

2.对于方法(如struct的方法),接收者为值类型时,可以直接用指针类型的变量调用方法,反过来同样也可以。

 package main
 ​
 //普通函数与方法的区别(在接收者分别为值类型和指针类型的时候)
 ​
 import (
     "fmt"
 )
 ​
 //1.普通函数
 //接收值类型参数的函数
 func valueIntTest(a int) int {
     return a + 10
 }
 ​
 //接收指针类型参数的函数
 func pointerIntTest(a *int) int {
     return *a + 10
 }
 ​
 func structTestValue() {
     a := 2
     fmt.Println("valueIntTest:", valueIntTest(a))
     //函数的参数为值类型,则不能直接将指针作为参数传递
     //fmt.Println("valueIntTest:", valueIntTest(&a))
     //compile error: cannot use &a (type *int) as type int in function argument
 ​
     b := 5
     fmt.Println("pointerIntTest:", pointerIntTest(&b))
     //同样,当函数的参数为指针类型时,也不能直接将值类型作为参数传递
     //fmt.Println("pointerIntTest:", pointerIntTest(b))
     //compile error:cannot use b (type int) as type *int in function argument
 }
 ​
 //2.方法
 type PersonD struct {
     id   int
     name string
 }
 ​
 //接收者为值类型
 func (p PersonD) valueShowName() {
     fmt.Println(p.name)
 }
 ​
 //接收者为指针类型
 func (p *PersonD) pointShowName() {
     fmt.Println(p.name)
 }
 ​
 func structTestFunc() {
     //值类型调用方法
     personValue := PersonD{101, "hello world"}
     personValue.valueShowName()
     personValue.pointShowName()
 ​
     //指针类型调用方法
     personPointer := &PersonD{102, "hello golang"}
     personPointer.valueShowName()
     personPointer.pointShowName()
 ​
     //与普通函数不同,接收者为指针类型和值类型的方法,指针类型和值类型的变量均可相互调用
 }
 ​
 func main() {
     structTestValue()
     structTestFunc()
 }
 ​
 /*
     输出结果:
         valueIntTest: 12
         pointerIntTest: 15
         hello world
         hello world
         hello golang
         hello golang
 */

你可能感兴趣的:(1024程序员节,golang)