【Go基础】01 基本语法

目录


学习资料
书籍:

  1. Go学习笔记–github雨痕
  2. golang学习笔记

1. 快速入门


1.1. 特性


1.2. 常用写法

参考:

  1. Go 学习笔记(26)— Go 习惯用法(

2. 类型

  1. 概述

    1. 按数据类型划分:
      1. 基本数据类型: int, float, string, bool
      2. 复合数据类型: array, slice, map, struct, interface …
    2. 按数据存储特点:
      1. 值类型:
        int, float64, bool, string, array …
      2. 引用类型: 操作数据的地址
        slice, map, chan
  2. 派生类型
    (a) 指针类型(Pointer)
    (b) 数组类型
    © 结构化类型(struct)
    (d) Channel 类型
    (e) 函数类型
    (f) 切片类型
    (g) 接口类型(interface)
    (h) Map 类型

2.1. 初始化顺序

  1. 初始化顺序:
    1. 当一个go源程序被初始化时,首先去初始化 所依赖的其他包
    2. 然后初始化该go源码文件的全局变量的初始化 和 执行初始化函数,
    3. 其中该包所有的全局变量初始化在前,该包的初始化函数init在后。
    4. 所有包的初始化函数执行完毕之后, 才执行main函数。
    5. 一个go源码文件中可以有多个初始化函数。但是不保证同一个代码中初始化函数的执行顺序。
    package main
    
    import "fmt"
    
    var a = hello()
    func hello() int {
       fmt.Println("hello")
       return 0
    }
    func init() {
       fmt.Println("world")
    }
    
    func main() {
       fmt.Println("Hello World")
       fmt.Println(a)   //return 0
    }
    
    //结果:
    hello
    world
    Hello World
    0
    

参考:

  1. golang中的init函数以及main函数

2.2. 变量

2.2.1. 变量定义

  1. 定义

    //1. 数组定义
    var variable_name [SIZE] variable_type
    balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
    var balance [10]float32
    b := [...]int{0: 1, 1: 2, 2: 3}    // 不定长数组;
    
    //2. 多维数组
    a := [...][2]int{
       {0: 1},
       {1: 2},
    }
    
  2. 匿名变量

    1. 匿名变量 _ 本身不会进行空间分配,也不会占用一个变量的名字;

2.2.2. 变量声明

  1. 当一个变量被声明之后,系统自动赋予它该类型的零值;

  2. 所有的内存在 Go 中都是经过初始化的。

    1. 与C不同, C 在变量声明时, 并不会对对应的内存区域进行清理工作;
  3. 多个 短变量 声明和赋值中, 至少有一个新声明的变量出现在左值中;

    1. 即便其他变量可能是重复声明的, 编译器也不会报错;
    conn, err := net.Dial("tcp","127.0.0.1:8080")
    conn2, err := net.Dial("tcp","127.0.0.1:8080")
    
  4. make() 与 new() 属于显式声明并初始化;

    // 全局变量声明
       --该写法一般用于声明全局变量
    var (
       name1 type
       name2 type
       )
    
    
  5. 类型与默认零值

    类型零值
    intfloat 0
    boolfalse
    string“” 空字符串
    struct内部字段的零值
    slicenil
    mapnil
    指针nil
    函数nil
    channil
    interfacenil

2.2.3. 初始化

  1. 初始化 赋值

    赋值操作符 = 和 := 的区别:
    = 不会声明并创建新变量,而是在当前赋值语句所在的作用域由内向外逐层去搜寻变量,
       如果没有搜索到相同的变量名,则报编译错误。
       
    := 必须出现在函数或者类型方法内部。
    := 至少要创建一个局部变量并初始化。
    
    // 示例:
    var g int, e int = 6, 7  // 多赋值语句中每个变量后面不能都带上类型   错误
             //  syntax error: unexpected comma at end of statement
    var a, b int = 1, 2
    var c, d = 3, 4          // 不带类型时,编译器自动推断
    
  2. comma,ok(, ok)表达式

    == 常见的 , ok 表达式 ==
    1. 获取map键值
    2. chan
    3. 类型断言
    
    v, ok = m[key]             // map lookup
    v, ok = x.(T)              // type assertion
    v, ok = <-ch               // channel receive
    
    v = m[key]                // map查找,失败时返回零值
    v = x.(T)                 // type断言,失败时panic异常
    v = <-ch                  // 管道接收,失败时返回零值(阻塞不算是失败)
    _, ok = m[key]            // map返回2个值
    _, ok = mm[""], false     // map返回1个值
    _ = mm[""]                // map返回1个值
    

参考:

  1. Go 习惯用法(多值赋值,短变量声明和赋值,简写模式、多值返回函数、comma,ok 表达式、传值规则)

2.2.4. 作用域

  • 全局作用域: 在任意命名空间可见

    1. Go 语言内置的预声明标识符(包括预声明的类型名、关键字、内置函数等)
    2. 包内以大写字母开头的标识符( 包括变量、常量、函数和方法名、自定义类型、结构字段等)
  • 包内作用域
    本包可见, 其他包不可见;
    1. 包内定义的以小写字母开头的标识符(变量、常量、函数和方法名、自定义类型、结构字段等〉
  • 隐式作用域

    1. 局部变量; 当前代码块可见;
  1. 不要将作用域和生命周期混为一谈:
    1. 作用域 是编译的属性;
    2. 生命周期是运行时变量的有效时间, 是一个运行时的概念;
      1. 注意 闭包 ;

2.3. 常量

2.3.1. 定义及初始化

  1. 常量值 必须是编译期可确定的数字、字符串、布尔值。

  2. 代码示例

    // 类型推断
    const s = "Hello World"
    
    const (           //常量组
       defaultMySQLConfigSection = "client"
       defaultConfigFile         = "~/.my.cnf"
       defaultBulkSize           = 1000
    )
    

2.3.2. iota

  • 概述:

    1. 每当 const 出现时, 都会使 iota 初始化为0
    2. const 中每新增一行常量声明将使 iota 计数一次.
    3. iota 是一个从 0 开始递增的整形值;
    4. 可以使用在 const块的任意位置; 其 取值 取决于iota所在的位置
  • 示例代码

    const a0 = iota // a0 = 0  // const出现, iota初始化为0
    const (
        i1, j1,k1 = iota, iota, iota    // 0 0 0 
                                        // 位于同一行, 只计算一次
    )
    
    // 递增特性
       const (
          a1 = iota   // a1 = 0   // 又一个const出现, iota初始化为0
          a2 = iota   // a1 = 1   // const新增一行, iota 加1
          a3 = 6      // a3 = 6   // 自定义一个常量
          a4          // a4 = 6   // 不赋值就和上一行相同
          a5 = iota   // a5 = 4   // const已经新增了4行, 所以这里是4
                                  // 0 1 2 3 4 
       )
    
    // 取值
       const (
       a = 4    // 显式的指定值
       b = 5    // 显式的指定值
       c = iota // c = 2,因为这里的 iota 位于第3个ConstSpec,2=3-1
       d        // d = 3,因为iota递增了1,等价于 d = iota
       )
    

参考:

  1. golang的iota问题
  2. 关于Golang中的iota

2.4. 字符串

参考:

  1. Go 学习笔记(31)— 字符串 string、字符 rune、字节 byte、UTF-8 和 Unicode 区别以及获取字符串长度

2.4.1. 字符串string

  • 概述:

    1. Go 语言中字符串的内部实现使用 UTF-8 编码,
    2. 通过 rune 类型,可以方便地对每个 UTF-8 字符进行访问。
    3. 当然, Go 语言也支持按照传统的 ASCII 码方式逐字符进行访问
  • 常用语法:

    // 底层实现
       // runtime/string.go
       type stringStruct struct {
          str unsafe . Pointer //指向底层字节数组的指针
          len int //字节数组长度
       }
    
    // 下标访问, 但不能修改;
       var a string = "hello,world"
       b := a[0]
       a[1] = "a" // error
    
    // 字符串判空:  2种方法
       var str string	//string 类型变量在定义后默认的初始值是空,不是 nil。
       if str == "" {
          // str 为空
       }
    
       if len(str) == 0 {
          // str 为空
       }
    
    
    
    ==注意:=
    1. len() 返回字符串的  字节数目  (不是rune字符数目);
    2. 字符串末尾不包含 NULL 字符,与 C/C++ 不一样;
    3. 获取字符串中某个字节的地址属于非法行为,例如 &str[i]4. 跨行的字符串使用 ``; 而不是""
    
  • 字符串转换

    1. 一个值在从 string 类型向 []byte 类型转换时代表着以 UTF-8 编码的字符串会被拆分成零散、独立的字节;
    2. 一个值在从 string 类型向 []rune 类型转换时代表着字符串会被拆分成一个个 Unicode 字符。

2.4.2. 字符rune类型

  • 字符编码:
    Go 默认的字符编码就是 UTF-8 类型。Go 语言的字符有以下两种:

    1. uint8 类型,或者叫 byte 型( byte 是 unit8 的别名),
      1. 代表了 ASCII 码的一个字符,占用 1 个字节。
    2. rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。
      1. rune 类型等价于 int32 类型,占用 4 个字节。
  • rune字符 概述

    1. rune 类型变量默认初始值是 0;

2.4.3. byte字节类型

  • 概述:

    1. byte 类型变量默认初始值是 0;
    2. byte 类型是 uint8 类型的一个别名;
  • 示例

    func main() {
       s := "hello 世界"
       runeSlice := []rune(s) // len = 8
       byteSlice := []byte(s) // len = 12
       // 打印每个rune切片元素
       for i:= 0; i < len(runeSlice); i++ {
          fmt.Println(runeSlice[i])
          // 输出104 101 108 108 111 32 19990 30028
       }
       fmt.Println()
       // 打印每个byte切片元素
       for i:= 0; i < len(byteSlice); i++ {
          fmt.Println(byteSlice[i])
          // 输出104 101 108 108 111 32 228 184 150 231 149 140
       }
    }
    

    补充: 字符串转换为 byte数组, 会发生内存拷贝吗?
    1. 会, 严格来说, 只要发生类型转换都会发生内存拷贝;
    2. 如何节省内存操作, 以提升性能?
    go // stringHeader 的地址 强转成 SliceHeader ==> 底层转换二者; a := "aaa" ssh := *(*reflect.StringHeader)(unsafe.Pointer(&a)) b := *(*[]byte)(unsafe.Pointer(&ssh))

  • 统计字符串长度的方法:
    当含有中文时:

      1. 使用 bytes.Count() 统计
      2. 使用 strings.Count() 统计
      3. 将字符串转换为 []rune 后调用 len 函数进行统计
      4. 使用 utf8.RuneCountInString()统计
    

2.5. 指针类型

2.5.1. 概述

  1. 使用建议:

    1. 不要对 map、slice、channel 这类引用类型使用指针;
    2. 大的结构体的参数传递, 考虑使用指针;
    3. int bool 小数据没有必要使用指针;
    4. 并发安全的前提下使用;
    5. 最好不用嵌套使用;
    6. 什么时候使用:
      1. 需要改变参数的值;
      2. 避免复制操作;
      3. 节省内存;
  2. 注意:

    1. Go 不支持 -> 操作符;
    2. Go 不支持指针运算;

2.5.2. 语法

  1. 基本语法:
    // 1. 声明:
       var ip *int        /* 指向整型*/
       var fp *float32    /* 指向浮点型 */
    
       -- 变量、指针和地址三者的关系是,每个变量都拥有地址,指针的值就是地址。
    // 2. 空指针
       var intP *int
       *intP =10  //====空指针nil 不能赋值;====
    // 3. 常用方法
       var intP *int = new(int)
       //更推荐简短声明法,这里是为了演示
       intP:=new(int)
    
    // 4. 判断:
       if(ptr != nil)  // ptr 不是空指针
    
    
    5. 指针数组
       var ptr [3]*int;       // 声明了整型 指针数组
       //数组指针
       var pf *[6]int = &arr
       //指针数组
       pfArr := [...]*int{&x,&y}
    

2.5.3. 转换


2.6. 自定义类型

2.6.1. 自定义类型

  1. 自定义类型与类型别名
    //类型定义
    type NewInt int
    
    //类型别名 : 与原类型等效;
    type MyInt = int
    
  2. 类型 与 新类型 区别:
    1. 其中 newType 是一种新的类型, newType 本身依然具备 Type 类型的特性。
    2. 类型声明语句一般出现在包一级,因此 如果新创建的类型名字的首字符大写,则在包外部也可以使用。
    3. 一个类型声明语句创建了一个新的类型名称,和现有类型具有相同的底层结构。
      若新命名的类型提供了一个方法,用来分隔不同概念的类型,这样即使它们底层类型相同也是不兼容的。
    // 类型定义
    type newType Type
    

参考:

  1. 李文周–结构体

2.6.2. type用法

  • type 有如下几种用法:
    定义结构体
    定义接口
    类型定义
    类型别名
    类型查询

2.7. 嵌入类型

2.7.1. 概述

  1. Go语言允许用户扩展或者修改已有类型的行为。–通过嵌入类型(type embdding)
  2. 嵌入类型是将已有的类型直接声明在新的结构类型里。

2.7.2. 示例

  1. 代码示例:

    // 实现 当内部类型和外部类型要
    // 实现同一个接口  ==== 实现同一个接口
    package main
    
    import (
       "fmt"
    )
    
    // notifier是一个定义了
    // 通知类行为的接口  == 接口定义的是行为;
    type notifier interface {
       notify()
    }
    
    // 定义user用户类型
    type user struct {
       name  string
       email string
    }
    
    // 通过user类型值的指针 调用方法
    func (u *user) notify() {
       fmt.Printf("Sending user email to %s<%s>\n",
          u.name,
          u.email)
    }
    
    // admin代表一个拥有权限的管理员用户
    type admin struct {
       user
       level string
    }
    
    // 通过admin类型值指针调用的方法
    func (a *admin) notify() {
       fmt.Printf("Sending admin email to %s<%s>\n",
          a.name,
          a.email)
    }
    
    // main()
    func main() {
       ad := admin{
          user: user{
             name:  "john smith",
             email: "john@yahoo.com",
          },
          level: "super",
       }
    
       // 给admin用户发送一个通知
       // 接口的嵌入的内部类型实现并没有提升到外部类型
       sendNotification(&ad)
    
       //直接访问内部类型的方法
       ad.user.notify()
    
       // 内部类型的方法没有提升
       ad.notify()
    }
    
    // 接受一个实现了notifier接口的值
    // 并发送通知
    func sendNotification(n notifier) {
       n.notify()
    }
    
    //输出
    // 该程序可以看出 admin类型如何实现 notifier接口, 以及
    // 如何由 sendNotification() 以及直接使用外部类型变量ad
    // 来执行 admin类型实现的方法;
    /*
    Sending admin email to john smith<john@yahoo.com>
    Sending user email to john smith<john@yahoo.com>
    Sending admin email to john smith<john@yahoo.com>
    */
    ==
    1. 多个同一类型嵌入时, 注意使用别名, 以区分调用;
    

总结:

  1. 若 外部类型 实现了notify方法, 内部类型的实现不会被提升;

  2. 但是 内部类型的值一直存在, 还可以通过直接访问内部类型的值,
    来调用没有被提升的内部类型实现的方法;

  3. 嵌入类型接口问题:

    1. 嵌入类型 接口: 内部类型实现的接口 会自动提升的外部类型;
      即: 内部类型实现了某个接口, 外部类型同样就实现了该接口;

2.7.3. 区分


2.8. 类型转换

2.8.1. 数字类型

  1. 尽管在某些特定的运行环境下 int 、 uint 和 uintptr 的大小可能相等,但是它们依然是不同的类型,比如 int 和 int32 ,虽然 int 类型的大小也可能是 32 bit,但是在需要把 int 类型当做 int32 类型使用的时候必须显示的对类型进行转换,反之亦然。
  • 整数类型转换
    1. 对于整数类型值整数常量之间的类型转换

      1. 原则上只要源值在目标类型的可表示范围内就是合法的。
      2. 当整数值的类型的有效范围由宽变窄时,只需在补码形式下截掉一定数量的高位二进制数即可。
    2. 内置的 len() 函数返回一个有符号的 int , 可以实现 for的逆序循环。

2.8.2. 等价类型

  1. Unicode 字符 rune 类型是和 int32 等价的类型,通常用于表示一个 Unicode 码点。
  2. byte uint8: byte 一般强调数字是一个原始数据, 而不是一个小整数;

2.8.3. 复数

  1. 内置的 complex 函数用于构建复数,内建的 real 和 imag 函数分别返回复数的实部和虚部:
    y := 3 + 4i
    

参考:

  1. Go 学习笔记(4)— Go 标识符、数据类型之间转换、布尔型、整型、浮点型、interface 类型 – 笔记很好;

3. 表达式


3.1. 关键字/保留字

  1. 概述
    var  const :变量和常量的声明
    var varName type  或者 varName : = value
    package and import: 导入
    func: 用于定义函数和方法
    return :用于从函数返回
    defer someCode :在函数退出之前执行
    go          用于并行
    select      用于选择不同类型的通讯
    interface   用于定义接口
    struct   用于定义抽象数据类型
    breakcasecontinueforfallthroughelseifswitchgotodefault   流程控制
    chan  用于channel通讯
    type  用于声明自定义类型
    map   用于声明map类型数据
    range 用于读取slice、map、channel数据
    

3.2. 运算符

3.2.1. 概述

  • 全部运算符 分隔符
    +  &  +=  &=  &&  ==  !=  (  )
    -  |  -=  |=  ||  <  <=  [  ]
    *  ^  *=  ^=  <-  >  >=  {  }
    /  <<  /=  <<=  ++  =  :=  ,  ;
    %  >>  %=  >>=  --  !  ...  .  :
    &^  &^=
    

3.2.2. 运算符 实战

  1. &^ 清楚标志位

  2. “++”、"–" 是语句 而非表达式。

    1. &^
       x = 1011
             &^
       y = 10010, 则使用 x 的值;
       ————————
       z = 0010
    2. ++ -- 自增自减
       ++i  错
       a=i++

3.3. 控制流

  1. 语法

    1. 不需要使用括号;
    
    
    
    var ten int = 11
    if ten > 10 {
       fmt.Println(">10")
    } else {
       fmt.Println("<=10")
    }
    
  2. lable标签:

    1. break 和标签一起使用,用于跳出标签所标识的 for 、 switch 、 select 语句的执行,可用于跳出多重循环,但标签和 break 必须在同一个函数内。
    2. continue 和标签一起使用,用于跳出标签所标识的 for 语句的本次选代,但标签和 continue 必须在同一个函数内。

3.3.1. range

  1. 概述

    1. 类似 迭代器操作,返回 (索引, 值) 或 (键, 值)。
    2. _ 可用于忽略 返回值;
    3. break 可用于 for、switch、select,而 continue 仅能用于 for 循环。
  2. 使用:

    1. Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或 集合(map)的元素。
    2. 在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
    // 遍历字符串
       for _, v := range s {
          if v > 127 {
             return false
          }
          if strings.Count(s, string(v)) > 1{
             return  false
          }
       }
    
  3. 迭代实质

    1. range 是使用一个副本重复赋值的方式来遍历每一个目标元素的,
    2. 可以将其视为一个目标元素类型的变量,每一次遍历迭代就会把目标元素拷贝到range准备的副本,并作返回。

3.3.2. switch

  • 语法:
    
    == 变量类型 ==
    // 在switch中使用 变量名.(type) 查询变量是由哪个类型数据赋值。
    // 每个case中含有一种情况
       var t interface{}
       t = functionOfSomeType()
       switch t := t.(type) {
       default:
          fmt.Printf("unexpected type %T\n", t)     // %T prints whatever type t has
       case bool:
          fmt.Printf("boolean %t\n", t)             // t has type bool
       case int:
          fmt.Printf("integer %d\n", t)             // t has type int
       case *bool:
          fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
       case *int:
          fmt.Printf("pointer to integer %d\n", *t) // t has type *int
       }
    

4. 数据

4.1. array数组

4.1.1. 概述

  1. 注意
    • 数组是值类型,赋值和传参会复制整个数组,⽽不是指针。
    • 数组⻓度必须是常量,且是类型的组成部分。[2]int 和 [3]int 是不同类型。
    数组的长度是数组类型的一个组成部分
    • ⽀持 “==”、"!=" 操作符,因为内存总是被初始化过的。
    • 指针数组 [n]*T,数组指针 *[n]T

  2. 语法

     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}, // 别忘了最后⼀⾏的逗号。
     }
    
    // 数组长度
       len(a)
    
    

4.2. 切片类型 slice

4.2.1. slice

  1. 概述

    1. Go 语言切片是对数组的抽象。
    2. Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go 中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。
  2. 特点

    1. 是一种引用;
  3. 语法

    // 结构:
    type slice struct {
       array unsafe.Pointer
       len   int
       cap   int
    }
    
    ==创建切片:==
       // 1. make方法
          make([]type, length, capacity)   //capacity是可选参数
             ==注意:==
                1. length: 分配元素的个数; capacity: 预分配的元素数量; 只是提前分配空间, 不影响 size;
                2. length 是数组的长度, 也是切片的长度;
                3. 使用make, 若不指定 capacity, 则 capacity=len
          b := make([]int, 2)
          b := make([]int, 2, 10)
    
       // 2. 常用方法: 从下标 startIndex 到 endIndex-1 下的元素 创建一个新的切片
          s := arr[startIndex:endIndex]
             var a  = [3]int{1, 2, 3}      //数组
             fmt.Println(a, a[1:2])        // [1 2 3]  [2]
                //==  1. 索引从 0 开始; 元素 自 startIndex 到 endIndex-1 
    
    • 创建切片:
      20210620114409
      20210620113728
    ==初始化:==
       data := [...]int{0, 1, 2, 3, 4, 5, 6} 
       slice := data[1:4:5]   // [low : high : max]  见下图:
          // 计算方法: 
          len = high - low
          cap = max - low
    
       // 切片初始化
          s1 := s[startIndex:endIndex]
    
    
    ==注意:==
    1.[] 运算符里指定了一个值,那么创建的就是数组而不是切片;
       // 创建有3个元素的整型数组
       array := [3]int{10, 20, 30}
    
       // 创建长度和容量都是3的整型切片  len=cap=3
       slice := []int{10, 20, 30}
    2. 使用 make() 函数生成的切片一定发生了内存分配; 但 slice := data[1:4:5] 的方法
       只是创建新的切片结构, 不会进行内存分配;
    
  4. 函数

    // 相关函数 builtin.go
       len()
       cap()
    
  5. 总结

    1. 数组 或 切片 生成新的切片拥有如下特性
      1. 取出的元素数量为:结束位置 - 开始位置;
      2. 取出元素不包含结束位置对应的索引,切片最后一个元素使用 slice[len(slice)] 获取;
      3. 当缺省开始位置时,表示从连续区域开头到结束位置;
      4. 当缺省结束位置时,表示从开始位置到整个连续区域末尾;
      5. 两者同时缺省时,与切片本身等效
  6. 实战:

    func main() {
       var s1 = []int{1, 2, 3, 4, 5, 6, 7}
       var s2 = s1[2:5]
       fmt.Println(s1)
       fmt.Println(s2)
       fmt.Println(cap(s1), cap(s2))
       /*
          unsafe.Pointer(&s1) 取到s1的首地址的指针, 其实也就是s1 array位置的指针
          unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s1))) 将s1 array存放的值取出来, 也就是底层数组的首元素的地址
          unsafe.Pointer(uintptr(s1SliceArrayPointer) + unsafe.Sizeof(&s1[0])*2) 向后偏移2位, 也就是取到3的位置
       */
       s1SliceArrayPointer := unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s1)))
       fmt.Println("s1[0]:", *(*int)(s1SliceArrayPointer))
       fmt.Println("s1 address:", s1SliceArrayPointer)
    
       s2SliceArrayPointer := unsafe.Pointer(*(*unsafe.Pointer)(unsafe.Pointer(&s2)))
       fmt.Println("s2[0]:", *(*int)(s2SliceArrayPointer))
       fmt.Println("s2 address:", s2SliceArrayPointer)
    
       offset2Pointer := unsafe.Pointer(uintptr(s1SliceArrayPointer) + unsafe.Sizeof(&s1[0])*2)
       fmt.Println("offset s1[2]:", *(*int)(offset2Pointer))
       fmt.Println("offset s1 address:", offset2Pointer)
    }
    // 结果: 
    [1 2 3 4 5 6 7]
    [3 4 5]
    7 5
    s1[0]: 1
    s1 address: 0xc000010240
    s2[0]: 3
    s2 address: 0xc000010250
    offset s1[2]: 3
    offset s1 address: 0xc000010250
    

参考:

  1. 深度解密Go语言之Slice

4.2.2. reslice

  1. 概述
    1. 基于已有的 slice , 创建新slice对象,以便在cap允许范围内调整属性;

4.2.3. append()

append属于内置函数,用于 slice 的元素添加操作。

  • append() 扩容
    1. 在无需扩容时,append 函数返回的是指向原底层数组的新切片;
    2. 需要扩容时,append 函数返回的是指向新底层数组的新切片。
    3. 扩容一般按照容量的两倍进行扩容;
      1. 当原切片的长度(以下简称原长度)大于或等于 1024 时,Go 语言将会以原容量的 1.25 倍作为新容量的基准。
      2. 若一次追加元素过多, 超过原容量的两倍, 则新容量以新长度为基准;
  1. 语法

    append() 支持链式操作, 可以将多个append()组合起来;
    //== 语法: ==
       slice = append(slice, elem1, elem2)
       slice = append(slice, anotherSlice...)  // 打散拼进去??
       // As a special case, it is legal to append a string to a byte slice, like this:
       slice = append([]byte("hello "), "world"...)
    
    //在中间插入元素
       var a []int
       a = append(a[:i], append([]int{x}, a[i:]...)...)      // 在第i个位置插入x
       a = append(a[:i], append([]int{1,2,3}, a[i:]...)...)  // 在第i个位置插入切片
    
    //示例:
       var a = []int{1, 2, 3}
       a = append([]int{0}, a...)          // 在开头添加1个元素
       a = append([]int{-3, -2, -1}, a...) // 在开头添加1个切片
       fmt.Println(a)                      // [-3 -2 -1 0 1 2 3]
    
       var b []int
       b = append(a[:0], append([]int{10}, b[0:]...)...)      // 在第0个位置插入10
       b = append(a[:0], append([]int{1, 2, 3}, b[0:]...)...) // 在第0个位置插入切片
       fmt.Println(b)                                         // [1 2 3 10]
    
  2. 切片的底层数组何时被替换??

    1. 确切地说,一个切片的底层数组永远不会被替换。为什么?虽然在扩容的时候 Go 语言一定会生成新的底层数组,但是它也同时生成了新的切片。它只是把新的切片作为了新底层数组的窗口,而没有对原切片,及其底层数组做任何改动。

注意:

  1. append() 属于命令,而非函数;具体函数实现: StackOverflow

4.2.4. copy()

  1. 语法:

    1. 在两个slice间复制数据; 复制长度以 len 小的为准;
    2. 两个slice 可以指向同一个底层数组, 允许元素区间重叠;
    //语法
    copy(destSlice, srcSlice) int
    
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := []int{5, 4, 3}
    
    copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
    copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
    
  2. 注意:

    1. 切片索引生成的切片与原来的切片是同一个地址;
      1. 多个切片指向同一底层数组,那么对其中一个切片的改变影响其它的切片。
      2. 即: 改变其中的值, 影响其他切片;
      s1 := []int{1,2,3,4,5}
      s2 := s1[0:3]     // 只是基于同一个底层数组生成了一个新的切片(或者说窗口)
      
    2. make() 会生成全新切片;
      s2 := make([]int, 3)      
      
  3. 思想

    1. 应及时将所需数据 copy 到较小的 slice,以便释放超大号底层数组内存。

4.2.5. 删除元素

  1. 语法
1. 头部删除
   // 1. 可以通过直接移动数据指针;
   a = []int{1, 2, 3}
   a = a[1:] // 删除开头1个元素
   a = a[N:] // 删除开头N个元素
   
   // 2. append() 不会导致内存空间变化;
   a = []int{1, 2, 3}
   a = append(a[:0], a[1:]...) // 删除开头1个元素
   a = append(a[:0], a[N:]...) // 删除开头N个元素

   // copy()
   a = []int{1, 2, 3}
   a = a[:copy(a, a[1:])] // 删除开头1个元素
   a = a[:copy(a, a[N:])] // 删除开头N个元素

2. 中间删除
   a = []int{1, 2, 3, ...}
   a = append(a[:i], a[i+1:]...) // 删除中间 1个元素
   a = append(a[:i], a[i+N:]...) // 删除中间 N个元素
   a = a[:i+copy(a[i:], a[i+1:])] // 删除中间 1个元素
   a = a[:i+copy(a[i:], a[i+N:])] // 删除中间 N个元素

3. 尾部删除
   a = []int{1, 2, 3}
   a = a[:len(a)-1] // 删除尾部1个元素
   a = a[:len(a)-N] // 删除尾部N个元素

4.3. Map

参考:

  1. title: Golang Map 实现 (四) --> 前面还有3篇

4.3.1. 语法

  1. 概述

    1. 引用类型, 哈希表;
    2. Map 是一种无序的 键值对 的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。
    3. Map 是一种集合,可以像迭代数组和切片那样迭代它。
      1. 不过,Map 是无序的,无法决定它的返回顺序,是因为 Map 是使用 hash 表来实现的。
  2. 特点

    1. 从 map 中取回的是一个value的临时复制品,对齐成员的修改无任何意义;
    2. map 被设计成 not addressable 通过原value 指针修改成员值, 会报错;
    3. 完整替换 value 或 使用 指针 ; --Go学习笔记;
  3. 语法格式

    /* 声明变量,默认 map 是 nil */
    var mapName map[mapKey]mapValue
    
    /* 使用 make 函数 */
    mapName := make(map[mapKey]mapValue)
    
    1. 初始化
       1. 如果不初始化 map ,那么就会创建一个 nil mapnil map 不能用来存放键值对。
       2. 使用内置函数 make 创建的 map 可以用于存储键值对。
       // nil 键值对
          // 通过声明映射创建一个  nil映射
          var book map[string]string
          book["author"] = "wohu"      // nil map 不能用来存放键值对;
    
          Runtime Error:
          panic: runtime error: assignment to entry in nil map
       
       //正确初始化: 
       d1 := make(map[string]string)
       var d2 = map[string]string{}    // 注意:后面带了个大括号
       d1["age"] = "18"
       d2["name"] = "wohu"
    
       // 示例
       maxValues      = map[string]int64{
          "tinyint":   0x7F,
          "smallint":  0x7FF,
          "mediumint": 0x7FFFF,
          "int":       0x7FFFFFF,
          "integer":   0x7FFFFFF,
          "float":     0x7FFFFFFF,
          "decimal":   0x7FFFFFFF,
          "double":    0x7FFFFFFF,
          "bigint":    0x7FFFFFFFFFFFFFF,
       }
    
    2. 查询
       // 第一种情况,断言查询
       val,ok := map[key]
    
       // 第二种情况直接查询
          -- 若键值不存在, val为默认零值
       val := map[key]
    
    3. 删除
       delete(map, key)
    4. 清空 map
       1. Go 语言中并没有为 map 提供任何清空所有元素的函数、方法,
       2. 清空 map 的唯一办法就是重新 make 一个新的 map3. 不用担心垃圾回收的效率, Go 语言中的并行垃圾回收效率比写一个清空函数要高效的多。
    
    5. 单 key 对应多个value    //1 key 多 value
       mp1 := make(map[int][]int)
       mp2 := make(map[int]*[]int)
    
    6. 追加元素
       someMap["someKey"]="someValue" 
    
    

4.3.2. 注意

  • 语法
    1. 
    2. 获取 键值对数量
    println(len(m))       //cap 无效
    
    
    
  1. Go 语言字典的键类型不可以是 函数类型、字典类型 和 切片类型。
    1. 要特别注意: 键的类型是接口类型时, 键值的实际类型也不能是以上三种;
  2. map 默认 无序, 与存储的顺序也没有关系;
  3. 键类型的 值 要支持判等操作; 即: 键的值之间可以使用 ==!=;

4.3.3. 并发安全

并发安全map – sync.map

参考:

  1. 深度解密Go语言之sync.map
  2. Go语言sync.Map(在并发环境中使用的map)

4.4. struct

4.4.1. 基本语法

  1. 概述

    1. 值类型, 赋值和传参会复制全部内容;
    2. 可以用 “_” 补位字段; 支持指向自身类型的指针;
  2. 特点:

    1. 顺序初始化 需要包含全部字段; 否则会出错;
    2. 只有当结构体实例化时,才会真正地分配内存;
  3. 语法

    1. 声明
       type Student struct {
          Id int
          Name string
          Age int
          Mobile int
          Major string
       }
    2. 创建变量
       // 创建Student指针类型变量
       s1 := new(Student)
       // 创建Student指针类型变量
       s2 := &Student{}
       // 创建Student类型变量
       s3 := Student{}
    
          补充: ==实例化与初始化==  java
          实例化   一般是由类创建的对象,在构造一个实例的时候需要 在内存中开辟空间,即   Student s = new Student();
          初始化   实例化的基础上,并且对 对象中的值进行赋一下初始值
    
    3. 实例化
       == 实例化 ==var的方式声明结构体即可完成实例化;
          var ins T
       1. 基本实例化
          type Point struct {
             X int
             Y int
          }
             
          var p Point  // p是值
          p.X = 10
          p.Y = 20
       2. 指针类型结构体 实例化
          ins := new(T)   // 形成指针类型的结构体; . 访问成员变量;
          type Player struct{
             Name string
             HealthPoint int
             MagicPoint int
          }
    
          tank :=new(Player)
          tank.Name = "Canon"
          tank.HealthPoint =300
          // 注: 结构体 和 结构体指针 是两种类型; 
          //    对于接口的实现来讲, 是不一样的
          
       3. 取地址实例化
          ins := &T{}   // 看做 new() 操作; 应用较广
          
          type Command struct{
             Name string
             Var     *int
             Comment string
          }
    
          var version int = 1
          cmd := &Command{}  // 取结构体地址进行实例化
          cmd.Name = "version"
          cmd.Var = &version
          cmd.Comment = "show version"
    
    
    
    4. 初始化:
       1. 键值对初始化--"填充字段多的情况"
          varName := structName{key1: value1, key2: value2..., keyn: valuen}
          // 键值之间以:分隔,键值对之间以,分隔。
          type People struct{
             name string
             child * People
          }
    
          relation := &People{
             name: "爷爷",
             child: &People{
                name: "爸爸",
                child: &People{
                      name: "我",
                },
             },
          }
    
       2. 列表初始化--"填充字段少的情况"
          --多个值使用 , 分隔开;
          ==注意: 初始化结构体所有字段, 顺序
          addr := Address{
             "四川",
             "成都",
             610000,
             "0",
          }
       3. 初始化匿名结构体
          ins := struct{
             //匿名结构体字段定义
             字段1 字段类型1
             字段2 字段类型2
             ...
          }{
             //字段初始化值-可选的
             初始化字段1: 字段1的值,
             初始化字段2: 字段2的值,
             ...
          }
    
    // 参考中 结构体嵌套 ==> 派生
    

参考:

  1. 结构体: 概念及初始化

4.4.2. 匿名结构体

  1. 语法
    1. 初始化
       // 实例化一个匿名结构体
       msg := &struct {     // 定义部分
          id   int
          data string
       }{  // 值初始化部分
          1024,
          "hello",
       }
    
    

4.4.3. 空结构体

  • 作用:

    1. 如果使用的是map,而且map又很长,通常会节省不少资源
    2. 空struct{}也在向别人表明,这里并不需要一个值, 仅包含需要方法
    // 空结构体 大小
       a := struct{}{}
       println(unsafe.Sizeof(a))     // Output: 0
    
       var x [1000000000]struct{}  
       fmt.Println(unsafe.Sizeof(x)) // prints 0
          // 切片元素不占用空间
    
    // 空结构体切片 
       var x = make([]struct{}, 1000000000)  //只是头部数据占用空间
       fmt.Println(unsafe.Sizeof(x)) // prints 12 in the playground
    
    // 空结构体 定义方法
       type Lamp struct{}
    
       func (l Lamp) On() {
             println("On")
    
       }
       func (l Lamp) Off() {
             println("Off")
       }
    
       func main() {
                // Case #1.
                var lamp Lamp
                lamp.On()
                lamp.Off()
                // Output:
                // on
                // off
          
                // Case #2.
                Lamp{}.On()
                Lamp{}.Off()
                // Output: 
                // on
                // off
       }
    
    

参考: 参考描述很棒

  1. 空结构体struct{}解析
  2. Golang空结构体struct{}用途,你知晓么?

4.4.4. 其他用法

  • 结构体中包含map
    1. 正确示例1 
       type Querystruct {
          Attributes []string
          Modifiers map[string][]Modifier
       
          Sources      map[string][]string
          SourceAliasesmap[string]string
       
          ConditionTree *ConditionNode
       }
    
       // 取地址初始化
       func NewQuery() *Query {
          return &Query{
             Attributes: make([]string, 0),
             Modifiers:  make(map[string][]Modifier),
             Sources:map[string][]string{
                   "include": make([]string, 0),
                   "exclude": make([]string, 0),
             },
             SourceAliases: make(map[string]string),
             ConditionTree: nil,
          }
       }
    
    2. 错误示例:
       type Param map[string]int
       type Show struct {
          Param
       }
    
       func main(){
          s:=new(Show)
          s.Param ["RMB"]=1000
          fmt.Println(*s)
       }
       // new关键创字无法初始化 Show 结构体中的 Param 属性;
       // 直接使用 s.Param 会报错;
    

5. 函数 方法 接口

  1. 函数&方法&接口 区别:
-- 函数
func function_name( [parameter list] ) [return_types] {
   函数体
}
--方法:
func (variable_name variable_data_type) function_name() [return_type]{
   /* 函数体*/
}:
  1. 参数列表() 在函数名前面;



5.1. 函数


5.1.1. 函数概述

5.1.1.1. 概述
  1. 函数的本质:

    1. 函数作为一种复合数据类型, 可以看作是一种特殊的变量;
    2. 函数名(): 将函数进行调用;
    3. 函数名: 指向函数体的内存地址;
  2. 函数类型

    1. 有名函数
    2. 匿名函数
5.1.1.2. 函数定义
  1. 语法格式

    func function_name( [parameter list] ) [return_types] {
       函数体
    }
       func:函数由 func 开始声明
       function_name:  函数名称,函数名和参数列表一起构成了函数签名。
       parameter list: 参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。
                      参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。
       return_types:  返回类型,函数返回一列值。return_types 是该列值的数据类型。
                      有些功能不需要返回值,这种情况下 return_types 不是必须的。
       函数体:函数定义的代码集合
    
    eg:
    /* 函数返回两个数的最大值 */
    func max(num1, num2 int) int {
       /* 定义局部变量 */
       var result int
    
       if (num1 > num2) {
          result = num1
       } else {
          result = num2
       }
       return result
    }
    
    // 打印函数类型
       fmt.Printf("%T")
    

5.1.1.3. 参数传递
  1. 值传递: 传递数据的副本
    1. 值类型的数据, 默认是值传递: 基础类型, array, struct
  2. 引用传递: 传递的是数据的地址:
    1. 引用类型的数据, 默认使用引用传递: slice, map, chan
      new make 进行初始化

==================

  • 不定参数
    1. 定义:
      1. 不定参数又叫 可变参数, 指函数传入的参数是可变的;
    2. 本质: …type 本质上是一个数组切片, 即 []type; –reflect.Valueof(args).Kind() 查看类型是 slice
    3. 注意:
      1. 不定参数类型相同 且 必须是函数的最后一个参数;
      2. 切片可以作为参数传递给不定参数,切片名后要加上 … ;
      3. 形参为不定参数的函数和形参为切片的函数类型是不相同的;
    4. 可变参数存在 多种数据类型,则可以将可变参数类型设置成 interface{}
    5. 代码演示:
      // 声明: 
         param ...type
      // 可变参数的函数
         func myfunc(args ...int) {
            for _, arg := range args {
               fmt.Println(arg)
            }
         }
      // 参数传递
         n := []int{1, 2, 3, 4, 5}
         result := sumOne(n...) // 切片元素作为参数传递给函数的不定参数,需要在切片名后加上 ...
         fmt.Println("result is ", result)
         ret := sumTwo(n)	// 传递切片自身
      // 存在多种数据类型
         func demo(a string, i int, other ...interface{}) {  }
      

====================


5.1.1.4. 返回值
  1. return 用于函数返回值, 或终止当前函数

  2. 支持 多值返回;

    1. 支持有名的返回值,参数名就相当于函数体内最外层的局部变量,
    命名返回值变量 会被初始化为类型零值,最后的 return 可以不带参数名直接返回;
    // sum 相当于函数内的局部变量,被初始化为 0
    func add(a, b int) (sum int) {
       sum = a + b
       return  // return sum 的简写模式
       // sum := a + b  则相当于新声明一个 sum 变量名,原有的 sum 变量被覆盖
       // return sum    需要显式地调用 return sum
    }
    
    2. 支持 多返回值, 
    1. 不能使用 容器对象 接收多返回值, 只能使用 多个变量或 "_" 忽略;
    2. 多返回值 可直接作为其他函数调用实参;: 函数可以直接做为形参;
       x, _ := test()
       println(add(test()))
    
    
5.1.1.5. 其他
  1. 注意
    1. 参数列表 不支持默认值参数; Go 没有默认参数值;
    2. 不支持函数重载;
    3. 不支持 命名函数嵌套, 但支持匿名函数嵌套;
    4. 如果实参包括引用类型,如 指针、 slice、 map、 function、 channel 等类型,实参可能会由于函数的间接引用被修改。非引用实参通过值传递;
    // 1. 命名函数嵌套
       func add(a , b int) (sum int) {
          anonymous:= func(x , y int) int {
             return x + y
          }
          
          return anonymous(a , b)
       }
    
    

5.1.2. 函数调用

5.1.2.1. 其他使用
  1. 其返回值作为其他函数的入参;

  2. 有具体名称的函数 可以看做是函数类型的变量;

  3. Go语言中函数也是类型, 可以作为参数传递给别的函数;

    // 2. 有名函数可以直接赋值给变量;
       f := sum       // sum
       ret := f(3, 4) //通过该变量直接调用函数;
    // 3. 函数作为参数传递:
       n := foo(add, 1,2)  //add() 直接作为参数进行传递;
    
5.1.2.2. 延迟调用 defer
  1. 多个defer注册, 按照 FILO 顺序执行; 即使某一个出错也会继续执行;

  2. defer 函数必须先注册 才能执行;

    1. 若 defer 位于 return 之后, 因为defer没有注册, 不会被执行;
    func main() {
       defer func() {
          fmt.Println("First")
       }()
       // defer 会在return 之前执行;
       return
       //后面的都不会执行
       defer func() {
          fmt.Println("Second")
       }()
    
       fmt.Println("This is main func body")  //不会执行
    }
    
    // 包含调用函数
       defer calc("1", a, calc("10", a, b))  
       // defer 在定义的时候会计算好调⽤函数的参数 即: 先计算出calc("10", a, b)的值, 再进行注册;
    
  3. 主动调用 os.Exit(int) 退出进程时, defer即使已经注册, 也不会执行;

  4. 相对普通函数调用有一定的性能损耗; 不能在for循环中进行使用;

  5. defer 后面调用的函数如果有返回值, 返回值会被丢弃;

  6. 返回值

    1. defer 函数的 有名返回值可视为 引用返回, 能被defer修改;
    2. 匿名返回值时, 相当于拷贝赋值, 不能够被修改;
    // 有名返回值
    func func1() (i int){
       defer func() {
          i++ 
          fmt.Println("defer2:", i)
       }()    //声明匿名函数并直接执行
       return 0  //结果为1
    }
    
  7. defer 通常用于释放资源和错误处理;

    // 释放资源
       valueByKeyGuard.Lock()       // valueByKeyGuard sync.Mutex
       defer valueByKeyGuard.Unlock()
    
       return valueByKey[key]       
    // 打开文件
       f, err := os.Open(filename)
    
       if err != nil {
          return 0
       }
       // 延迟调用Close, 此时Close不会被调用
       defer f.Close() // defer 后的语句(f.Close())将会在函数返回前被调用,自动释放资源。
       // 不能将这一句代码放在 nil 之前,一旦文件打开错误,f 将为空,在延迟语句触发时,将触发宕机错误。
    
    

参考:

  1. Go 学习笔记(17)— 函数(03)[defer 定义、defer 特点、defer 释放资源]

5.1.3. 匿名函数

  1. 概述

    1. 匿名函数: 没有函数名的函数实现;
    2. 可以直接赋值给变量;
    3. 可以当 实参; 可以作为返回值;
    4. 可以直接被调用;
    //定义格式: 
       func(参数列表) (返回值){
          函数体
       }
    //定义时
       匿名函数用作回调函数
    
    // 声明匿名函数并直接执行
    func(args){
       ...CODE...
    }(parameters)
    

参考:

  1. Go 学习笔记(16)— 函数(02)[函数签名、有名函数、匿名函数、调用匿名函数、匿名函数赋值给变量、匿名函数做回调函数]

5.1.4. 闭包

  1. Go 语言闭包函数的定义:当匿名函数引用了外部作用域中的变量时就成了闭包函数,

    1. 闭包函数是函数式编程语言的核心; 闭包是运行期动态的概念;
    2. 也就是匿名函数可以会访问其所在的外层函数内的局部变量。
    3. 当外层函数运行结束后,匿名函数会与其使用的外部函数的局部变量形成闭包。
  2. 闭包:

    1. 一个外层函数中, 有内层函数, 该内层函数中, 会操作外层函数的局部变量(外层函数中的参数, 或者外层函数中直接定义的变量), 并且这个外层函数的返回值就是这个内层函数;
      这个内层函数和外层函数的局部变量, 统称 闭包结构;
    2. 局部变量的生命周期会发生改变, 闭包中的局部变量不会随着外层函数的结束而终止, 因为内层函数还要使用;–变量会跟随闭包的生命期一致存在;
    3. 因为匿名函数捕捉的局部变量的声明周期随闭包一直存在, 所以
      参考: 千锋教育–闭包
  3. 闭包结构

    1. 当是两个不同的闭包实例时, 拥有不同的地址; 闭包的记忆将重新开始;
    func add() func(int) int {
       var x int
       return func(y int) int {
          x += y
          return x
       }
    }
    func main() {
       var f = add()
       fmt.Println(f(1)) //1
       fmt.Println(f(2)) //3
       fmt.Println(f(3)) //6
    
       f1 := add()
       fmt.Println(f1(4)) //4
       fmt.Println(f1(5)) //9
    }
    

参考:

  1. Go 学习笔记(18)— 函数(04)[闭包定义、闭包修改变量、闭包记忆效应、闭包实现生成器、闭包复制原对象指针]1

5.1.5. 错误处理

io.EOF是io包中的变量, 表示文件结束的错误:

  • panic()

    1. 谨慎使用 panic() 抛出异常, 如果没有捕获异常, 将会导致程序异常退出;
    2. panic() 发生前注册的defer, 会在panic() 之前 执行;
    3. 异常 可以直接调用 panic() 产生, 也可以有运行时的错误产生;(如: 越界访问数组)
  • recover()

    1. 该函数可以让 宕机流程中的 goroutine 恢复过来;
    2. 注意:
      1. 仅在 defer 中有效;
      2. 当前 goroutine 陷入panic, 调用recover() 可以捕捉panic的输入值, 并恢复到正常执行;
         1. recover 只有在 defer 调用的函数内部时,
             才能阻止 panic 抛出的异常信息继续向上传递;
      3. 延迟调用中引发的错误, 可被后续延迟调用捕获, 但仅 `最后一个错误可被捕获`;
      4. 任何未捕获的错误都会沿调用堆栈向外传递。
      
    3. go 语言中的recover() 的宕机恢复机制就对应其他语言中的 try/catch 机制;
  1. 代码演示

    // recover() 捕捉异常
    package main
    
    import "fmt"
    
    func test() {
       defer func() {
          fmt.Println(recover())
       }()
       defer func() {
          panic("defer panic")  //仅最后一个panic会被捕捉;
       }()
       panic("test panic")
    }
    func main() {
       test()
    }
    
    // 结果输出
    $ go run  test_recover.go
    defer panic
    
  2. 实战应用

    package main
    
    import (
       "fmt"
       "time"
    )
    
    // 抛出异常,模拟实际 Panic 的场景
    func throwException() {
       panic("An exception is thrown! Start Panic")
    }
    
    // Go 的 defer + recover 来捕获异常
    func catchExceptions() {
       defer func() {
          if e := recover(); e != nil {
                fmt.Printf("Panicing %s\n", e)
          }
       }()
       go func() {
          // 做具体的实现任务
          fmt.Print("do something \n")
       }()
    
       throwException()
       fmt.Printf("Catched an exceptions\n")
    }
    
    func main() {
       fmt.Printf("==== start main =====\n")
    
       // 执行一次
       catchExceptions()
    
       num := 1
       for {
          num++
          fmt.Printf("\nstart circle num:%v\n", num)
    
          // 循环执行,如果实际项目中,这个函数是主任务的话,需要一个 for 来循环执行,避免捕获一次 Panic 之后就不再继续执行
          catchExceptions()
          time.Sleep(3 * time.Second)
          if num == 5 {
                fmt.Printf("==== end main =====\r\n")
                return
          }
       }
    }
    

参考:

  1. Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]

5.1.6. 高阶函数 回调函数

  1. 概述
    1. 高阶函数: 接收一个函数作为参数;
    2. 回调函数: 作为另一个函数参数的函数;

5.2. 方法

5.2.1. 方法定义

  1. 简介:

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

    --方法:
    type mytype struct{}
    func (recv mytype) my_method(para) return_type {}
    func (recv *mytype) my_method(para) return_type {}
    
    func (variable_name variable_data_type) function_name() [return_type]{
       /* 函数体*/
    }:
    1. 参数列表() 在函数名前面;
    
    
    -- 函数
    func function_name( [parameter list] ) [return_types] {
       函数体
    }
    
    --接口
    type interface_name interface {
       method_name1 [return_type]
       method_name2 [return_type]
       method_name3 [return_type]
       ...
       method_namen [return_type]
    }
    // 具体见接口
    
    

5.2.2. 匿名字段

5.2.3. 方法集

  1. 指针与值的问题:
    20210319104852

  2. 方法调用

    1. 值接收者使用值的副本来调用方法,而指针接受者使用实际值来调用方法

5.3. 接口

5.3.1. 接口定义

  1. 概述

    1. 描述1
      1. Go 语言提供了另外一种 数据类型 即接口,它把所有的具有共性的方法定义在一起;
      2. 任何其他类型只要实现了这些方法就是实现了这个接口。 – 类型与类型之间
    2. 描述2
      1. 接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,
      2. 而是通过方法由用户定义的类型实现。 --<Go语言实战>
  2. 格式

    /* 定义接口 */
    type interface_name interface {
      method_name1 [return_type]
      method_name2 [return_type]
      method_name3 [return_type]
      ...
      method_namen [return_type]
    }
    
    //定义接口
    type Stringer interface {
      String() string
    }
    
    /* 定义结构体 */
    type struct_name struct {
      /* variables */
    }
    
    /* 实现接口方法 */
    func (struct_name_variable struct_name) method_name1() [return_type] {
      /* 方法实现 */
    }
    ...
    func (struct_name_variable struct_name) method_namen() [return_type] {
    /* 方法实现*/
    }
    
  3. 接口示例

    package main
    
    import (
       "fmt"
    )
    
    type Phone interface {   //phone接口 -- 特殊类型
       call()               // 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()
    
    }
    // 备注:
    定义了一个接口Phone,接口里面有一个方法call()。\
    然后在main函数里面定义了一个Phone类型变量,并分别为之赋值为NokiaPhone和IPhone。
    然后调用call()方法,输出结果如下:
    
    I am Nokia, I can call you!
    I am iPhone, I can call you!
    

参考:

  1. Go 语言接口–菜鸟教程
  2. 对C++和Go语言中接口的理解

5.3.2. 类型断言

  • 语法格式:
    // 需转换为接口, 再进行断言
    // 1. 直接赋值
       st := &St{"abcd"}
       var i interface{} = st
       // 判断i 绑定的实例是否实现了接口类型Inter
       o := i.(Inter)
    //2. comma,ok 表达式
       var i1, ok := interface{}(i).(TypeNname)
    
    // comma, ok 表达式
       func main() {
          st := &St{"abcd"}
          var i interface{} = st
          // 判断i 绑定的实例是否实现了接口类型Inter
          if o, ok := i.(Inter); ok {
             o.Ping()
             o.Pang()
          }
    
          //  i 没有实现接口 Anter
          if p, ok := i.(Anter); ok {
             p.String()
          }
    
          // 判断 i 绑定的实例是否就是具体类型 St
          if s, ok := i.(*St); ok {
             fmt.Printf("%s", s.Name)
          }
       }
    

5.3.3. 执行机制

  1. 接口表(interface table)指针

  2. 数据指针

    1. 目标对象的 制度复制品, 复制完整对象或指针;

5.3.4. 接口转换

5.3.5. 接口区分:

5.3.5.1. C++
  1. golang 实现类和抽象接口之间不需要硬性连接(即声明继承或虚函数),只要你的实现类实现了某接口规定的方法,那么该类的使用方就可以实例化该类并赋值给接口,然后可以通过接口直接调用具体方法。
  2. C++
    1. C++的接口可被称之为抽象类,即设定一个抽象基类去描述一组基本行为(接口),然后通过多样化的派生类去完成该接口。
    2. 接口作为不同组件之间的契约存在,且对契约的实现是强制的,即语言必须声明实现了该接口。

参考:

  1. 对C++和Go语言中接口的理解

5.4. 公开或未公开的标志符

  1. 概述
    1. Go语言支持从包里公开或隐藏标志符;

5.5. 示例

// 包 间访问结构体类型




==说明:==
1. 由于内部类型 user 未公开, 无法直接通过结构字面量的方式初始化该内部类型;
2. 内部类型里面的字段是公开的,由于提升到外部, 可以通过外部类型 来访问;


6. Go特性


6.1. 面向对象

  • 面向对象编程:
    Go 面向对象编程的三大特性:封装、继承和多态。

    1. 封装:隐藏对象的属性和实现细节,仅对外提供公共访问方式
    2. 继承:使得子类具有父类的属性和方法或者重新定义、追加属性和方法等
    3. 多态:不同对象中同种行为的不同实现方式
  • Go编程

    1. Go 语言放弃了包括继承在内的大量面向对象特性,只保留了组合(composition)这个最基础的特性。

参考:

  1. Go 学习笔记(36)— 基于Go方法的面向对象(封装、继承、多态)

6.1.1. 封装

6.1.2. 继承

  • 概述:

    1. 确切地说,Go 语言也提供了继承,但是采用了组合的文法,所以我们将其称为匿名组合,Go 语言的继承方式采用的是匿名组合的方式。
  • 代码示例

    // 结构体之间的组合, 方法的隐藏
    package main
    
    type Base struct {
       Name string
    }
    
    func (base *Base) Foo() { ... }
    func (base *Base) Bar() { ... }
    
    type Foo struct {
       Base
       ...
    }
    
    func (foo *Foo) Bar() {
       foo.Base.Bar()
       ...
    }
    

6.1.3. 多态

  • 概述

    1. 在面向对象中,多态的特征为:不同对象中同种行为的不同实现方式。在 Go 语言中可以使用接口实现这一特征。
  • 示例代码

    package main
    
    import (
       "fmt"
    )
    
    // 正方形
    type Square struct {
       side float32
    }
    
    // 长方形
    type Rectangle struct {
       length, width float32
    }
    
    // 接口 Shaper
    type Shaper interface {
       Area() float32
    }
    
    // 计算正方形的面积
    func (sq *Square) Area() float32 {
       return sq.side * sq.side
    }
    
    // 计算长方形的面积
    func (r *Rectangle) Area() float32 {
       return r.length * r.width
    }
    
    func main() {
    // 创建并初始化 Rectangle 和 Square 的实例,由于这两个实例都实现了接口中的方法,
    //所以这两个实例,都可以赋值给接口 Shaper 
       r := &Rectangle{10, 2}
       q := &Square{10}
    
       // 创建一个 Shaper 类型的数组
       shapes := []Shaper{r, q}
       // 迭代数组上的每一个元素并调用 Area() 方法
       for n, _ := range shapes {
          fmt.Println("矩形数据: ", shapes[n])
          fmt.Println("它的面积是: ", shapes[n].Area())
       }
    }
    
    /*
    矩形数据:  &{10 2}
    它的面积是:  20
    图形数据:  &{10}
    它的面积是:  100
    */
    

6.2. 泛型

空接口

  • 概述:
    1. Go 语言没有泛型, 如果一个函数需要接收任意类型的参数, 则参数类型可以使用空接口类型
      1. 空接口不是真的为空,接口有类型和值两个概念

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值