YUKIPEDIA's blog

一个普通的XMUER

《Summer Pockets》久島鴎推し


Go基础学习笔记

目录

[TOC]

主要特征

  • 语法简单,自带 gc
  • 静态编译,编译好后在服务器直接运行
  • 简单的思想,没有继承、多态、类等
  • 语法层支持并发,拥有同步并发的 channel 类型,使并发开发变得非常方便
  • 内置类型丰富,函数多返回值
  • 反射

Golang 内置类型和函数

内置类型

值类型

bool
int(32 or 64), int8, int16, int32, int64
uint(32 or 64), uint8(byte), uint16, uint32, uint64
float32, float64
string
complex64, complex128
array    -- 固定长度的数组

引用类型(指针类型)

slice   -- 序列数组(最常用)
map     -- 映射
chan    -- 管道

内置函数

Go 语言拥有一些不需要进行导入操作就可以使用的内置函数。它们有时可以针对不同的类型进行操作,例如:len、cap 和 append,或必须用于系统级的操作,例如:panic。因此,它们需要直接获得编译器的支持。

append          -- 用来追加元素到数组slice中,返回修改后的数组slice
close           -- 主要用来关闭channel
delete            -- 从map中删除key对应的value
panic            -- 停止常规的goroutine  panic和recover用来做错误处理
recover         -- 允许程序定义goroutine的panic动作
imag            -- 返回complex的实部   complexreal imag用于创建和操作复数
real            -- 返回complex的虚部
make            -- 用来分配内存返回Type本身(只能应用于slice, map, channel)
new                -- 用来分配内存主要用来分配值类型比如intstruct返回指向Type的指针
cap                -- capacity是容量的意思用于返回某个类型的最大容量只能用于切片和 map
copy            -- 用于复制和连接slice返回复制的数目
len                -- 来求长度比如stringarrayslicemapchannel 返回长度
printprintln     -- 底层打印函数在部署环境中建议使用 fmt 

内置接口

type error interface { //只要实现了Error()函数,返回值为String的都实现了err接口

        Error()    String

}

init 函数和 main 函数

init 函数

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

init 函数有如下特征:

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

main 函数

Go语言程序的默认入口函数(主函数):func main()

func main() {
	// 函数体
}

两种函数的异同

  • 相同点:
    • 两个函数在定义时不能有任何的参数和返回值,且 Go 程序自动调用
  • 不同点:
    • init 函数可以应用于任意包中,且可以重复定义多个
    • main 函数只能用于 main 包中,且只能定义一个
  • 两个函数的执行顺序:
    • 同一个 go 文件中的 init() 调用顺序是从上到下
    • 同一个 package 中不同文件是按文件名字符串比较 “从小到大” 顺序调用各文件中的 init() 函数
    • 对于不同的 package ,如果不相互依赖的话,按照 main 包中 “先 import 的后调用” 的顺序调用包中的 init() ,如果 package 存在依赖,则先调用最早被以来的 package 中的 init() ,最后调用 main 函数

运算符

  1. 算术运算符
运算符 描述
+ 相加
- 相减
* 相乘
/ 相除
% 求余

注意: ++(自增)和–(自减)在Go语言中是单独的语句,并不是运算符

  1. 关系运算符
运算符 描述
== 检查两个值是否相等,如果相等返回 True 否则返回 False
!= 检查两个值是否不相等,如果不相等返回 True 否则返回 False
> 检查左边值是否大于右边值,如果是返回 True 否则返回 False
>= 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False
< 检查左边值是否小于右边值,如果是返回 True 否则返回 False
<= 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False
  1. 逻辑运算符
运算符 描述
&& 逻辑 AND 运算符。 如果两边的操作数都是 True,则为 True,否则为 False
|| 逻辑 OR 运算符。 如果两边的操作数有一个 True,则为 True,否则为 False
! 逻辑 NOT 运算符。 如果条件为 True,则为 False,否则为 True
  1. 位运算符
运算符 描述
& 参与运算的两数各对应的二进位相与(两位均为1才为1)
| 参与运算的两数各对应的二进位相或(两位有一个为1就为1)
^ 参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1(两位不一样则为1)
« 左移 n 位就是乘以 2 的 n 次方。“a « b” 是把 a 的各二进位全部左移 b 位,高位丢弃,低位补 0
» 右移 n 位就是除以 2 的 n 次方。“a » b” 是把 a 的各二进位全部右移 b 位
  1. 赋值运算符
运算符 描述
= 将一个表达式的值赋给一个左值
+= 相加后再赋值
-= 相减后再赋值
*= 相乘后再赋值
/= 相除后再赋值
%= 求余后再赋值
«= 左移后赋值
»= 右移后赋值
&= 按位与后赋值
|= 按位或后赋值
^= 按位异或后赋值

下划线

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

  1. 下划线在 import

    • 当导入一个包时,该包下的文件里所有的 init() 函数都会被执行。如果我们仅仅希望它执行 init() 函数,不想把整个包都导入进来,就可以使用 import _ 包路径 来引用该包

    • 示例 1:

      • 代码结构

        src 
        |
        +--- main.go            
        |
        +--- hello
               |
                +--- hello.go
        
        package main
               
        import _ "./hello"
               
        func main() {
            // hello.Print() 
            //编译报错:./main.go:6:5: undefined: hello
        }
        
      • hello.go

        package hello
               
        import "fmt"
               
        func init() {
            fmt.Println("imp-init() come here.")
        }
               
        func Print() {
            fmt.Println("Hello!")
        }
        
      • 输出结果:

        imp-init() come here.
        
    • 示例 2:

      import "database/sql"
      import _ "github.com/go-sql-driver/mysql"
      

      第二个 import 就是不直接使用 mysql 包,只是执行这个包的 init() 函数,把 mysql 的驱动注册到 sql 包里,然后程序里就可以使用 sql 包来访问 mysql 数据库了

  2. 下划线在代码中

    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("xxxxxx")
      • 如果此时不需要知道返回的错误值,就可以用:f, _ := os.Open("xxxxxx") ,如此就忽略了 error 变量
    • 解释 2:下划线作为占位符,意思是那个位置本应赋给某个值,但是咱们不需要这个值,所以就把该值赋给下划线,意思是丢掉不要,这样编译器可以更好的优化。
      • 任何类型的单个值都可以丢给下划线,这种情况是占位用的,比如方法返回两个结果,而你只想要一个结果,那另一个就用 “_” 占位

变量与常量

变量

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

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

var 变量名 变量类型

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

var name string
var age int
var ok bool

每声明一个变量就写一个 var 比较繁琐,Go 语言还支持批量变量声明:

var (
	a string
	b int
	c bool
	d float32
)

Go 语言在声明变量的时候,会自动对变量对应的内存区域进行初始化操作。每个变量会被初始化成其类型的默认值,例如: 整型和浮点型变量的默认值为 0 ,字符串变量的默认值为空字符串,布尔型变量默认为false。切片、函数、指针变量的默认为 nil

当然我们也可在声明变量的时候为其指定初始值。变量初始化的标准格式如下:

var 变量名 类型 = 表达式

// example
var name string = "hello"
var sex int = 1

或者一次初始化多个变量:

var name, sex = "hello", 1

类型推导:有时候我们会将变量的类型省略,这个时候编译器会根据等号右边的值来推导变量的类型完成初始化

var name = "hello"
var sex = 1

短变量声明:在函数内部,可以使用更简略的 := 方式声明并初始化变量

package main

import (
    "fmt"
)
// 全局变量m
var m = 100

func main() {
    n := 10
    m := 200 // 此处声明局部变量m
    fmt.Println(m, n)
}

匿名变量:在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿名变量用一个下划线 _ 表示

func foo() (int, string) {
    return 10, "hello"
}
func main() {
    x, _ := foo()
    _, y := foo()
    fmt.Println("x=", x)
    fmt.Println("y=", y)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明(在 Lua 等编程语言里,匿名变量也被叫做哑元变量)

注意事项:

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

常量

相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。 常量的声明和变量声明非常类似,只是把 var 换成了 const常量在定义的时候必须赋值

const pi = 3.1415
const e = 2.7182

声明了 pie 这两个常量之后,在整个程序运行期间它们的值都不能再发生变化了

多个常量也可以一起声明:

const (
	pi = 3.1415
	e = 2.7182
)

const 同时声明多个常量时,如果省略了值则表示和上面一行的值相同

const (
	n1 = 100
	n2
	n3
)

常量 n1n2n3 的值均为 100

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

const (
	n1 = 100 // 0
	n2		 // 1
	n3		 // 2
)

使用 _ 跳过某些值:

const (
	n1 = 100 // 0
	n2		 // 1
	_		 
	n3		 // 3
)

使用示例:

const (
	n1 = iota // 0
	n2 = 100
	n3 = iota // 2
	n4 // 3
)
const n5 = iota // 0

定义数量级:

const (
    _  = iota
    KB = 1 << (10 * iota)
    MB = 1 << (10 * iota)
    GB = 1 << (10 * iota)
    TB = 1 << (10 * iota)
    PB = 1 << (10 * iota)
)

多个 iota 定义在一行:

const (
    a, b = iota + 1, iota + 2 //1,2
    c, d                      //2,3
    e, f                      //3,4
)

基本类型

  1. 整形:整型分为以下两个大类: 按长度分为:int8int16int32int64对应的无符号整型:uint8uint16uint32uint64 。其中,uint8 就是 byte 型,int16 对应C语言中的 short 型,int64 对应C语言中的 long

  2. 浮点型:Go语言支持两种浮点型数:float32float64 。这两种浮点型数据格式遵循 IEEE 754 标准: float32 的浮点数的最大范围约为3.4e38,可以使用常量定义:math.MaxFloat32float64 的浮点数的最大范围约为 1.8e308,可以使用一个常量定义:math.MaxFloat64

  3. 复数complex64complex128 ,复数有实部和虚部,complex64 的实部和虚部为 32 位,complex128 的实部和虚部为 64 位

  4. 布尔值:Go语言中以 bool 类型进行声明布尔型数据,布尔型数据只有 truefalse 两个值

    • 注意:布尔类型变量的默认值为 falseGo 语言中不允许将整型强制转换为布尔型,布尔型无法参与数值运算,也无法与其他类型进行转换
  5. 字符串:Go 语言中的字符串以原生数据类型出现,使用字符串就像使用其他原生数据类型(int、bool、float32、float64 等)一样。 Go 语言里的字符串的内部实现使用 UTF-8 编码。 字符串的值为双引号中的内容,可以在Go语言的源码中直接添加非 ASCII 码字符,例如:

    s1 := "hello"
    s2 := "你好" // 非 ASCII码
    
  6. 多行字符串:Go 语言中使用反引号定义多行字符串,例如:

    s1 := `第一行
    第二行
    第三行
    `
    fmt.Println(s1)
    
  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. byte 和 rune 类型

    • 组成每个字符串的元素叫做“字符”,可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号 包裹起来,如:

      var a := '中'
      var b := 'x'
      
    • Go 语言的字符有以下两种:

      • uint8 类型,或者叫 byte 型,代表了 ASCII 码的一个字符
      • rune 类型,代表一个 UTF-8 字符
    • 当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型实际是一个 int32 。 Go 使用了特殊的 rune 类型来处理 Unicode,让基于 Unicode 的文本处理更为方便,也可以使用 byte 型进行默认字符串处理,性能和扩展性都有照顾

      // 遍历字符串
      func traversalString() {
          s := "pprof.cn博客"
          for i := 0; i < len(s); i++ { //byte
              fmt.Printf("%v(%c) ", s[i], s[i])
          }
          fmt.Println()
          for _, r := range s { //rune
              fmt.Printf("%v(%c) ", r, r)
          }
          fmt.Println()
      }
      

      输出如下:

      112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 229(å) 141() 154() 229(å) 174(®) 162(¢)
      112(p) 112(p) 114(r) 111(o) 102(f) 46(.) 99(c) 110(n) 21338(博) 23458(客)
      
      • 因为 UTF8 编码下一个中文汉字由 3~4 个字节组成,所以我们不能简单的按照字节去遍历一个包含中文的字符串,否则就会出现上面输出中第一行的结果
      • 字符串底层是一个 byte 数组,所以可以和 []byte 类型相互转换。 rune 类型用来表示 UTF-8 字符,一个 rune 字符由一个或多个 byte 组成,注意:字符串是不能修改的
  2. 修改字符串:要修改字符串,需要先将其转换成 []rune[]byte ,完成后再转换为string。无论哪种转换,都会重新分配内存,并复制字节数组

    func changeString() {
        s1 := "hello"
        // 强制类型转换
        byteS1 := []byte(s1)
        byteS1[0] = 'H'
        fmt.Println(string(byteS1)) // 输出:Hello
       
        s2 := "博客"
        runeS2 := []rune(s2)
        runeS2[0] = '狗'
        fmt.Println(string(runeS2)) // 输出:狗客
    }
    
  3. 类型转换:Go 语言中只有强制类型转换,没有隐式类型转换。该语法只能在两个类型之间支持相互转换的时候使用

    • 基本语法:

      T(表达式)
      

      其中,T表示要转换的类型。表达式包括变量、复杂算子和函数返回值等

      比如计算直角三角形的斜边长时使用 math 包的 Sqrt() 函数,该函数接收的是 float64 类型的参数,而变量 a 和 b 都是 int 类型的,这个时候就需要将 a 和 b 强制类型转换为 float64 类型

      func sqrtDemo() {
          var a, b = 3, 4
          var c int
          // math.Sqrt()接收的参数是float64类型,需要强制转换
          c = int(math.Sqrt(float64(a*a + b*b)))
          fmt.Println(c)
      }
      

数组 Array

  1. 数组定义:

    var a [len]int
    
    • 比如:var a [5]int ,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变
    • 长度是数组类型的一部分,因此,var a[5]intvar a[10]int 是不同的类型
    • 数组是值类型,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值
    • 指针数组 [n]*T,数组指针 *[n]T
  2. 一维数组

    // 全局:
    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"}
    // 局部:
    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}, // 别忘了最后一行的逗号。
    }
    
    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)
    }
    
    输出结果:
    [1 2 3 0 0] [1 2 3 4 5] [1 2 3 4 5 6] [   hello world tom]
    [1 2 0] [1 2 3 4] [0 0 100 0 200] [{user1 10} {user2 20}]
    
  3. 多维数组 & 多维数组遍历

    • 参考:https://www.topgoer.com/go%E5%9F%BA%E7%A1%80/%E6%95%B0%E7%BB%84Array.html
  4. 数组拷贝和传参

    package main
       
    import "fmt"
       
    func printArr(arr *[5]int) {
        arr[0] = 10
        // 循环中 i 是元素下标,v 是元素数值
        for i, v := range arr {
            fmt.Println(i, v)
        }
    }
       
    func main() {
        var arr1 [5]int
        printArr(&arr1)
        fmt.Println(arr1)
        arr2 := [...]int{2, 4, 6, 8, 10}
        printArr(&arr2)
        fmt.Println(arr2)
    }
    
    输出结果:
    0 10
    1 0
    2 0
    3 0
    4 0
    [10 0 0 0 0]
    0 10
    1 4
    2 6
    3 8
    4 10
    [10 4 6 8 10]
    

切片 Slice

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

    • 切片是数组的一个引用,但自身是结构体,通过值拷贝传递

    • 切片的长度可以改变,因此切片是一个可变数组

    • 切片遍历方式和数组一样,可以用 len() 求长度,表示可用元素数量,读写操作不能超过该限制

    • cap() 可以求出切片最大扩张容量,不能超出数组限制:0 <= len(slice) <= len(array) ,其中 arrayslice 引用的数组

    • 定义:var 变量名 []类型 ,比如:var str []stringvar arr []int

    • 如果 slice == nil ,那么 len()、cap() 结果都等于 0

  2. 创建切片的各种方式

    package main
       
    import "fmt"
       
    func main() {
       //1.使用 var 声明切片 s1,s1 是一个 nil 切片,没有分配内存
       var s1 []int
       if s1 == nil {
          fmt.Println("是空")
       } else {
          fmt.Println("不是空")
       }
       // 2.使用 := 简短声明,创建切片 s2 并初始化为空切片
       // 空切片与 nil 切片不同,空切片底层数组已分配内存,但是容量为 0,而 nil 切片没有分配内存
       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)
    }
    

    注意:切片的长度是当前切片存储的元素数量,通过 len() 获取切片长度;切片容量是从切片的起始位置到底层数组的末尾的元素数量,也就是切片能容纳的元素的最大数量,通过 cap() 获取切片长度

  3. 切片初始化

    // 全局:
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    var slice0 []int = arr[start:end] 
    var slice1 []int = arr[:end]        
    var slice2 []int = arr[start:]        
    var slice3 []int = arr[:] 
    var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
    // 局部:
    arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
    slice5 := arr[start:end]
    slice6 := arr[:end]        
    slice7 := arr[start:]     
    slice8 := arr[:]  
    slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
    

    image.png

    package main
       
    import (
        "fmt"
    )
       
    var arr = [...]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
    var slice0 []int = arr[2:8]
    var slice1 []int = arr[0:6]        //可以简写为 var slice []int = arr[:end]
    var slice2 []int = arr[5:10]       //可以简写为 var slice[]int = arr[start:]
    var slice3 []int = arr[0:len(arr)] //var slice []int = arr[:]
    var slice4 = arr[:len(arr)-1]      //去掉切片的最后一个元素
    func main() {
        fmt.Printf("全局变量:arr %v\n", arr)
        fmt.Printf("全局变量:slice0 %v\n", slice0)
        fmt.Printf("全局变量:slice1 %v\n", slice1)
        fmt.Printf("全局变量:slice2 %v\n", slice2)
        fmt.Printf("全局变量:slice3 %v\n", slice3)
        fmt.Printf("全局变量:slice4 %v\n", slice4)
        fmt.Printf("-----------------------------------\n")
        arr2 := [...]int{9, 8, 7, 6, 5, 4, 3, 2, 1, 0}
        slice5 := arr[2:8]
        slice6 := arr[0:6]         //可以简写为 slice := arr[:end]
        slice7 := arr[5:10]        //可以简写为 slice := arr[start:]
        slice8 := arr[0:len(arr)]  //slice := arr[:]
        slice9 := arr[:len(arr)-1] //去掉切片的最后一个元素
        fmt.Printf("局部变量: arr2 %v\n", arr2)
        fmt.Printf("局部变量: slice5 %v\n", slice5)
        fmt.Printf("局部变量: slice6 %v\n", slice6)
        fmt.Printf("局部变量: slice7 %v\n", slice7)
        fmt.Printf("局部变量: slice8 %v\n", slice8)
        fmt.Printf("局部变量: slice9 %v\n", slice9)
    }
    
    输出结果:
    全局变量:arr [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice0 [2 3 4 5 6 7]
    全局变量:slice1 [0 1 2 3 4 5]
    全局变量:slice2 [5 6 7 8 9]
    全局变量:slice3 [0 1 2 3 4 5 6 7 8 9]
    全局变量:slice4 [0 1 2 3 4 5 6 7 8]
    -----------------------------------
    局部变量: arr2 [9 8 7 6 5 4 3 2 1 0]
    局部变量: slice5 [2 3 4 5 6 7]
    局部变量: slice6 [0 1 2 3 4 5]
    局部变量: slice7 [5 6 7 8 9]
    局部变量: slice8 [0 1 2 3 4 5 6 7 8 9]
    局部变量: slice9 [0 1 2 3 4 5 6 7 8]