Go(GoLang)语言基础、知识速查

Go语言基础

认识

Go(又称Golang) 由Google开发,于2009年首次公开发布。它旨在解决C++编译慢、并发复杂等问题以及提供简洁、高效、可靠的软件开发解决方案。

Golang由Google工程师Robert Griesemer、Rob Pike和Ken Thompson于2007年设计,2009年正式开源。

Golang是一种静态强类型、编译型、并发型 编程语言,特别适合 云计算、微服务、分布式系统 等领域。

支持最强大的并发、内存管理、垃圾回收机制等。

下载安装

官网: go.dev/(英)

可访问 中国镜像站 golang.google.cn/

安装教程参考:learn.microsoft.com/zh-cn/azure...

以上步骤操作完重新打开VSCode就可以使用了

Go语言声明

  • var(声明变量) 变量意为 可变的东西,就是赋值完还能继续赋值改变这个值 Go语言的变量声明格式为:var 变量名 变量类型 = 值

    go 复制代码
    package main
    
    func main() {
    	var a int = 10 // 声明一个整型变量a并赋值为10
    	var b int // 声明一个整型变量b,未赋值,默认值为0
    	var c = 20 // 为声明类型,go会根据值自动推断类型
    	// 短声明(语法糖 :=  与上面意思一致)
    	d := "短声明d" // 短声明一个d变量,赋值为字符串类型
    	// 一行声明个多,相同类型或不同类型的变量
    	var aa,bb,cc int = 11,"22",33
    	// 批量声明
    	var (
    	  e int = 1
    	  f = 2
    	)
    }

    变量作用域

    go 复制代码
    package main
    
    import (
        "fmt"
    )
    // 全局变量m
    var m = 100
    
    func main() {
        n := 10
        m := 200 // 此处声明局部变量m
        fmt.Println(m, n)
    }
  • const(声明常量) 相对于变量,常量是恒定不变的值,多用于定义程序运行期间不会改变的那些值。只是把var换成了const,常量在定义的时候必须赋值。

    go 复制代码
    const pi = 3.1415
     const e = 2.7182
     // 批量声明
     const (
        pi = 3.1415
        e = 2.7182
    	)
     // 使用iota 关键字进行递增计数
     //  iota 在const关键字出现时将被重置为 0
      const (
    		n1 = iota //0
    		n2        //1
    		n3        //2
    		n4        //3
    	)

关键字

关键字是 Go 语言中预先保留的单词,在程序中有特殊含义,不能用来定义变量或常量名字。

break default func interface select
case defer go map struct
chan else goto package switch
const fallthrough if range type
continue for import return var

数据类型

Go 语言中数据类型分为:基本数据类型复合数据类型

  • 基本数据类型 整型、浮点型、布尔型、字符串
  • 复合数据类型 数组、切片、结构体、函数、map、通道(channel)、接口等

基本数据类型

整型

整型的类型有很多中。我们可以根据具体的情况来进行定义 有符号整型int8,int16,int32,int64 无符号整型 : uint8, uint16, uint32, uint64

提示!

  • 有符号(Signed)能赋值正数负数,但正数范围小。
  • 无符号(Unsigned)只能赋值正数,但能存更大的正数。

如果我们直接写int也是可以的,它在不同的电脑操作系统中,int的大小是不一样的

32位操作系统:int -> int32 64位操作系统:int -> int64

go 复制代码
var num8 uint8 = 128
var num16 uint16 = 32768
var num32 uint32 = math.MaxUint32
var num64 uint64 = math.MaxUint64
浮点型

浮点型表示存储的数据是实数,如3.145

32位操作系统:float32 64位操作系统:float64

go 复制代码
var num1 float32 = math.MaxFloat32
var num2 float64 = math.MaxFloat64

提示: 我们知道浮点数能表示的数值很大,但是浮点数的精度却没有那么大:

  • float32 的精度只能提供大约 6 个十进制数(表示小数点后 6 位)的精度。
  • float64 的精度能提供大约 15 个十进制数(表示小数点后 15 位)的精度。
布尔值

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

go 复制代码
var d bool = true
f := false

注意 布尔类型变量的默认值为false。

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

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

字符串

字符串的值为双引号(" ")中的内容

go 复制代码
s1 := "hello"
s2 := "你好"

复合数据类型

数组

是一种数据类型固定长度的序列,可以理解为一个存放数据的容器。

  • 数组定义:var a [len]int,比如:var a [5]int,一旦定义,长度不能变。
  • 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
  • 数组是值类型赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
go 复制代码
// 数组初始化
var arr1 = [3]int{1, 2, 3}
fmt.Println(arr1) // [1, 2, 3]

// 短声明
arr2 := [3]int{4, 5, 6}
fmt.Println(arr2) // [4, 5, 6]

// 部分初始化,为初始化为0值
arr3 := [5]int{1, 2}
fmt.Println(arr3) // [1, 2, 0, 0, 0]

// 通过指定索引,方便对数组某几个元素赋值
arr4 := [5]int{0: 1, 3: 4}
fmt.Println(arr4) // [1, 0, 0, 4, 0]

// 根据初始化的值,指定长度
arr5 := [...]int{1, 2, 3}
fmt.Println(arr5) // [1, 2, 3]

// 多维数组
var arr6 = [2][3]int{{1, 2, 3}, {4, 5, 6}}
fmt.Println(arr6) // [[1 2 3] [4 5 6]]
切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。

切片是一个引用类型,它的内部结构包含指针、长度和容量。切片一般用于快速地操作一块数据集合。

格式var name []T

  • name:表示变量名
  • T:表示切片中的元素类型

切片的定义方式与数组的定义方式的区别在于,数组在初始化的时候我们将一个具体大小给他设定了,而切片没有定义大小。

使用内置len()函数求长度 使用内置cap()函数求容量

go 复制代码
func main() {
	var a []string              //声明一个字符串切片
	var b = []int{}             //声明一个整型切片并初始化
	var c = []bool{false, true} //声明一个布尔切片并初始化
	// 使用内置 make([]T, len, cap) 定义切片
	numList := make([]int, 3, 5)
	fmt.Println(numList) // [0 0 0]
	fmt.Println(len(numList)) // 3
	fmt.Println(cap(numList)) // 5
	// 通过数组进行切片截取
	a := [5]int{1, 2, 3, 4, 5}
	// 切片的底层就是一个数组,所以我们可以基于数组通过切片表达式得到切片(通过索引截取)
	// 格式:数组变量[起始位置:结束位置]
	// 切片中不包含结束位置的元素
	s := a[1:3]
	fmt.Println(s) // [2 3]
	
	/*
		a[2:]  // 等同于 a[2:len(a)]
		a[:3]  // 等同于 a[0:3]
		a[:]   // 等同于 a[0:len(a)]
	*/
	
	// 创建多维切片
	nameList := [][]string{
		{"1", "张三"},
		{"2", "李四"},
		{"3", "王二"},
		{"4", "麻子"},
	}	
	fmt.Println(nameList) // [[1 张三] [2 李四] [3 王二] [4 麻子]]
}
用append内置函数操作切片(切片追加)
go 复制代码
package main
import (
    "fmt"
)
func main() {
    var a = []int{1, 2, 3}
    fmt.Printf("slice a : %v\n", a) // [1 2 3]
    var b = []int{4, 5, 6}
    fmt.Printf("slice b : %v\n", b) // [4 5 6]
    c := append(a, b...)
    fmt.Printf("slice c : %v\n", c) // [1 2 3 4 5 6]
    d := append(c, 7)
    fmt.Printf("slice d : %v\n", d) // [1 2 3 4 5 6 7]
    e := append(d, 8, 9, 10)
    fmt.Printf("slice e : %v\n", e) // [1 2 3 4 5 6 7 8 9 10]
}

超出原 slice.cap 限制,就会重新分配底层数组,即便原数组并未填满, 通常以 2 倍容量重新分配底层数组

指针

指针也是一种类型,也可以创建变量,称之为指针变量。指针变量的类型为 *Type,该指针指向一个 Type 类型的变量。指针变量最大的特点就是存储的某个实际变量的内存地址,通过记录某个变量的地址,从而间接的操作该变量。

指针声明获取格式

  • var 变量名 *类型 = new(类型)
  • 填入值:*变量名 = 值
  • 获取指针:&变量名
  • 获取值:*变量名
go 复制代码
package main
import (
    "fmt"
)
func main() {
	var num int = 10
	p := &num // 将地址值指针赋值给p变量
	fmt.Println(p) // 输出地址值指针 0x14000010230
	fmt.Println(*p) // 通过指针访问值  // 10
	fmt.Println(&num) // 0x14000010230
	fmt.Println(num) // 10
	

	// 修改指针指向的值
	*p = 20
	fmt.Println(num) // 20

	// new 先创建指针分配好内存,再给指针写入值
	var p1 *int = new(int)
	*p1 = 30
	fmt.Println(*p1) // 30
}

只需要记住两个符号:&(取地址)和*(根据地址取值)。 总结 : 取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。

变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  1. 对变量进行取地址(&)操作,可以获得这个变量的指针变量。 >2. 指针变量的值是指针地址。 >3. 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
空指针

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

go 复制代码
package main

import "fmt"

func main() {
    var p *string
    fmt.Println(p)
    fmt.Printf("p的值是%s/n", p)
    if p != nil {
        fmt.Println("非空")
    } else {
        fmt.Println("空值")
    }
}
Map

map是一种无序的基于key-value的数据结构,Go语言中的map是引用类型。它类似于其他编程语言中的哈希表或字典,提供了快速的插入、删除和查找操作

map中的数据是无序排列 map中的key只能是string|int类型

go 复制代码
package main
import (
    "fmt"
)
func main() {
	// 定义方式
	// make方式  格式:name := make(map[KeyType]ValueType) 或者 make(map[KeyType]ValueType, [cap])
	// KeyType:表示键的类型。 ValueType:表示键对应的值的类型。 cap表示容量
	make1 := make(map[string]string) 
	fmt.Printf(make1) // map[]
	
	// 通过字面量
	var userInfo1 map[string]string = map[string]string{
        "username": "zhangsan",
        "password": "123456",
    }
	// 通过短声明
	userInfo2 := map[string]string{
        "username": "zhangsan",
        "password": "123456",
    }
    fmt.Printf(userInfo2) // map[username:zhangsan password:123456]
    
    userList := map[int]string{
	    1: '张三',
	    2: '李四',
	    3: '王二',
    }
    // 添加元素到map
	userList[4] = "麻子"
	fmt.Println(userList) // map[1:张三 2:李四 3:王二 4:麻子]
	// 更新map
	userList[4] = "mazi"
	fmt.Println(userList) // map[1:张三 2:李四 3:王二 4:mazi]
	// 获取元素
	fmt.Println(userList[4]) // mazi
	// 删除元素
	delete(userList, 4)
	fmt.Println(userList) // map[1:张三 2:李四 3:王二]
	
	//判断键值是否存在  value,ok := map[key]
	u3, ok := userList[3]
	fmt.Println(ok) // true
	fmt.Println(u3) // 张三

	u4, ok := userList[4]
	fmt.Println(ok) // false
	fmt.Println(u4) // 
	
	// 循环map
	for key, value := range m1 {
		/*
		%s、%d都是占位符,%s是用于插入字符串类型,%d用于插入整数类型的值
		*/
		fmt.Printf("key: %s, value: %d\n", key, value)
	}
	
}

结构体

理解

  • Go语言中没有"类"的概念,也不支持"类"的继承等面向对象的概念。Go语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。
  • Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

简单理解就是go语言中的类成为结构体,用typestruct关键字进行实现

go 复制代码
type 类型名 struct {
	字段名 字段类型
	字段名 字段类型
	...
}
/*
1.类型名:标识自定义结构体的名称,在同一个包内不能重复。
2.字段名:表示结构体字段名。结构体中的字段名必须唯一。
3.字段类型:表示结构体字段的具体类型。
*/

结构体中字段大写开头表示可公开访问,小写表示私有(仅在定义当前结构体的包中可访问)。

go 复制代码
package main

import "fmt"

// 创建结构体
type Person struct {
	name string // 姓名
	age  int    // 年龄
}

type Person2 struct {
	name, age string // 声明多个同类型字段
	sex       int
}




func main(){
	// 实例化结构体
	// 按字段名称对每个字段进行初始化
	persion := Person{name: "张三", age: 30}
	fmt.Println(persion) // {张三 30}

	// 按字段顺序进行初始化
	persion2 := Person2{"李四", "30", 1}
	fmt.Println(persion2) // {李四 30 1}
	
	
	// 创建并实例化结构体
	persion3 := struct {
	  name string
	  age int
	  sex string
	}{
	  name: "王二",
	  age: 20,
	  sex: "男"
	}
	fmt.Println(persion3) // {王二 20 男}
	
	// 实例化时未给值
	var person22 = Person2{}
	fmt.Println(person22) // {  0}
	
	// 实例化未给全值
	var person222 = Person2{"李四", "30"}
	fmt.Println(person22) // {李四 30 0}
	
	// 通过 . 的方式对实例化后的结构体 进行改 删 查
	person22.name = "李四"
	person22.age = "30"
	person22.sex = 1
	fmt.Println(person22) // {李四 30 1}
	
	// 匿名属性结构体 多个相同类型会报错
	type Person3 struct {
		string
		int
	}
	
	var person33 = Person3{"王五", 40}
	fmt.Println(person33.string) // 王五
	fmt.Println(person33.int) // 40
	fmt.Println(person33) // {王五 40}
	
	// 获取指针指针地址
	var person333 = &Person33{"王五", 30}
	fmt.Println(person333) // {王五 30}
	fmt.Println((*person333).int) // 30
	fmt.Println(person333.int) // 30
	
}

方法和接收者

Go语言中的方法(Method )是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver )。 接收者的概念就类似于其他语言中的this或者 self。

定义格式

go 复制代码
func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
	函数体
}

解析

  • 接收者类型 就是定义的结构体
  • 接收者变量 与定义普通变量相似就是随意定义一个名字,建议使用接收者类型的第一个字母 小写
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。函数后面会细讲

例子

go 复制代码
//Person 结构体
type Person struct {
    name string
    age  int8
}


// 将Dream方法 绑定到Person 结构体下;可以理解为  Dream方法是属于Person的
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func (p Person) SetName(n string) {
 // 这种方式不会修改实例后结构体的数据
 p.name = n
}

func (p *Person) SetName2(n string) {
 // 这种使用指针接收器可以直接修改原实例数据
 p.name = n
}



func main() {
	var p1 = Person{name: "zhangsan", age: 24}
    p1.Dream()
}

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

函数

函数用于功能代码块的封装,使用func关键字定义 格式

go 复制代码
func 函数名(参数列表, ...) 返回值类型 {
	函数体
}

参数解析

  • 函数名:随便起名字,通常以小驼峰命名,比如:funcName
  • 参数列表:由一个或多个参数名 参数类型 组成,比如: a int,b string;函数可以没有参数或接受多个参数。定义的这些参数相当于定义的变量,只有在当前函数内部使用;可以传递任意类型的参数
  • 返回值类型:函数可以返回任意数量的返回值
  • 函数体:编写正常的go语言代码即可

例子

go 复制代码
package main

import "fmt"

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


// 一个返回值类型可以省略括号
func sum(x int, y int) int {
	return x + y
}

//无参数和返回值
func printNum() {
	fmt.Println("go go go")
}


// 以下方式的args都是一个slice(切片),可以用切片的方式去操作
// 注意点:在参数类型前面加 ... 表示一个切片,用来接收调用者传入的参数,切片参数必须放在参数列表最后
func myfunc1(args ...int) {    //0个或多个int参数
}

func add1(a int, args...int) int {    //1个或多个int参数
}

func add2(a int, b int, args...int) int {    //2个或多个int参数
}

// 使用...interface{}的方式可以传递多个任意类型的参数
func myfunc2(args ...interface{}) {
}

// 返回值添加变量
func test2(a, b int) (sum int, avg int) {
	sum = a + b
	avg = sum / 2
	return
}


func main() {
	testReturn, testStr := test(1, 2, "3")
	fmt.Println(testReturn, testStr) // 3 3
	fmt.Println(sum(1, 2)) // 3
	printNum()
	myfunc1()
	myfunc2(1, true, "张三")
	
	sum, avg := test2(10, 20)
	fmt.Println(sum, avg) // 30 15
}

注意点

  • 基本类型参数传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

  • 引用类型参数传递:是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
    函数可见性

  • 函数名首字母大写,对所有的包都是public,其他包可以导入当前包使用

  • 函数名首字母小写,当前函数是private,其他包无法访问

匿名函数

格式

go 复制代码
func (参数列表, ...) 返回值类型 {
	函数体
}

多用于创建变量在把匿名函数赋值,函数当参数进行传递时

go 复制代码
package main

import (
    "fmt"
    "math"
)

func main() {
    getSqrt := func(a float64) float64 {
        return math.Sqrt(a)
    }
    fmt.Println(getSqrt(4)) // 2
    
    // 匿名函数并自执行
    result := func(a, b int) int {
	    return a + b
	}(3, 4)
	fmt.Println(result) // 输出 7
	
	// 命名返回值
	func fun4() (res string) {
	  return // 相当于先定义再赋值
	  //return "abc"
	}
}

闭包、递归

闭包:与其他语言闭包概念一致,函数嵌套函数,内部函数引用外部函数的变量形成引用环境

go 复制代码
package main

import (
    "fmt"
)
func add(base int) func(int) int {
    return func(i int) int {
        base += i
        return base
    }
}

func main() {
	tmp1 := add(10)
	fmt.Println(tmp1(1), tmp1(2)) // 11 13
}

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

go 复制代码
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)) // Factorial of 7 is 5040
}

延迟调用(defer)

简单点说就是 defer 语句后面跟着的函数会延迟到当前函数执行完后再执行。

只能用于函数、结构体方法

go 复制代码
package main

import "fmt"

func bookPrint() {
    fmt.Println("bookPrint方法")
}

func main() {
	defer bookPrint()
    fmt.Println("main函数...")
}
/*
 会先输出 main函数...
 在输出   bookPrint方法
*/

注意点:

  • 使用 defer 只是延时调用函数,传递给函数里的变量,不应该受到后续程序的影响。
  • defer 不仅能够延迟函数 的执行,也能延迟方法的执行。
  • 当一个函数内多次调用 defer 时,Go 会把 defer 调用放入到一个栈中,随后按照 后进先出 的顺序执行。

包(package)

Go 语言是使用包来组织源代码的, **包(package)**是多个 Go 源码的集合,一个包可以简单理解为一个存放多个.go 文件的文件夹。该文件夹下面的所有 go 文件都要在代码的第一行添加如下代码,声明该文件归属的包。

Golang 中的包可以分为三种:1、系统内置包 2、自定义包 3、第三方包

系统内置包 : Golang 语言给我们提供的内置包,引入后可以直接使用,如 fmt、strconv、strings、 sort、errors、time、encoding/json、os、io 等。 自定义包 :开发者自己写的包 第三方包:属于自定义包的一种,需要下载安装到本地后才可以使用,如 "github.com/shopspring/decimal"包解决 float 精度丢失问题。

在上面讲述的代码中都会看到,package main这段代码,这就是声明当前.go文件的包名

格式

  • 声明当前.go文件的包名: package pacakgeName
  • 在当前文件中导入其他包时:import "a/b/c",import后面是包的相对路径,从当前项目的根目录下指定

标识符可见性

如果想在一个包中引用另外一个包里的标识符(如变量、常量、类型、函数等)时,该标识 符必须是对外可见的(public)。

在 Go 语言中只需要将标识符的首字母大写就可以让标识符对外可见了。

定义一个名为calc 的包

go 复制代码
package calc
 
//首字母大小表示公有,首字母小写表示私有
 
var a = 100 //私有变量
var Age = 20 //公有变量
 
func Add(x, y int) int {
    return x + y
}
func Sum(x, y int) int {
    return x - y
}

main.go中引入这个包

go 复制代码
package main
 
import (
    "fmt"
    "demo02/calc"
    ca "demo02/calc" // 有别名的包
)
 
func main(){
    c := calc.Add(10,20)
    c2 := ca.Add(10,20)
    fmt.Println(c)
}

init() 函数

每个包文件都有个固定的,init函数此函数没有参数也没有返回值 ,充当每个包的生命周期初始化,不能在代码中主动调用它。

编译执行顺序⬇️

流程控制

条件语句

格式

go 复制代码
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}

例子

go 复制代码
package main

import "fmt"

func main() {

   /* 定义局部变量 */
   var a int = 10
   /* 使用 if 语句判断布尔表达式 */
   if a < 20 {
       /* 如果条件为 true 则执行以下语句 */
       fmt.Printf("a 小于 20\n" ) //  a 小于 20
   }
   fmt.Printf("a 的值为 : %d\n", a) // a 的值为 : 10

	score := 88  
	if score >= 90 {  
	    fmt.Println("成绩等级为A")  
	} else if score >= 80 {  
	    fmt.Println("成绩等级为B")  
	} else if score >= 70 {  
	    fmt.Println("成绩等级为C")  
	} else if score >= 60 {  
	    fmt.Println("成绩等级为D")  
	} else {  
	    fmt.Println("成绩等级为E 成绩不及格")  
	}
	
	// 简便用法
	if score := 88; score >= 60 {  
	    fmt.Println("成绩及格")  
	}

}

选择语句

与其他语言的switch相似

格式

go 复制代码
switch var1 {
  case val1:
  ...
  case val2:
  ...
  default:
  ...
}

例子

go 复制代码
package main

import "fmt"

func main() {
   /* 定义局部变量 */
   var grade string = "B"
   var marks int = 90

   switch marks {
      case 90: grade = "A"
      case 80: grade = "B"
      case 50,60,70 : grade = "C"
      default: grade = "D"  
   }

	//无表达式的switch
   switch {
      case grade == "A" :
         fmt.Printf("优秀!\n" )     
      case grade == "B", grade == "C" :
         fmt.Printf("良好\n" )      
      case grade == "D" :
         fmt.Printf("及格\n" )      
      case grade == "F":
         fmt.Printf("不及格\n" )
      default:
         fmt.Printf("差\n" )
   }
   fmt.Printf("你的等级是 %s\n", grade )
   
   
   // 结合fallthrough关键字
    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")
    }
   
   
   // 简便用法
   switch month := 5; month {
		case 1, 3, 5, 7, 8, 10, 12:
		    fmt.Println("该月份有 31 天")
		case 4, 6, 9, 11:
		    fmt.Println("该月份有 30 天")
		case 2:
		    fmt.Println("该月份闰年为 29 天,非闰年为 28 天")
		default:
		    fmt.Println("输入有误!")
	}
}

循环语句

在Go语言中只有一种循环for

以下是常用的几种格式

格式

go 复制代码
for initialisation; condition; post {
  code
}
// for 接一个条件表达式
for condition {
  code
}
// for 接一个 range 表达式
for range_expression {
  code
}
// for 不接表达式
for {
  code
}

例子

go 复制代码
package main

import "fmt"

func main() {
	// 定义变量、条件判断、变量自增/自减 都放在一起,与其他语言类似
	for num := 0; num < 4; num++ {
	    fmt.Println(num)
	}
	
	// 接一个条件表达式
	num := 0
	for num < 4 {
	    fmt.Println(num)
	    num++
	}
	
	// 接一个 range 表达式
	// for 循环的 range 格式可以对 切片(slice)、map、数组、字符串等进行迭代循环。
	 s := "abc"
    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)
    }
    
    // !!注意,因a变量是数组,修改操作不会改变原数组。======
    a := [3]int{0, 1, 2}

    for i, v := range a { // index、value 都是从复制品中取出。

        if i == 0 { // 在修改前,我们先修改原数组。
            a[1], a[2] = 999, 999
            fmt.Println(a) // 确认修改有效,输出 [0, 999, 999]。
        }

        a[i] = v + 100 // 使用复制品中取出的 value 修改原数组。

    }

    fmt.Println(a) // 输出 [100, 101, 102]。
	
	//for 不接表达式,以下写法会无限循环,可以使用 break 关键字结束
	// 第一种写法
	for {
	    code
	}
	// 第二种写法
	for ;; {
	    code
	}
}
  • 循环语句支持以下控制关键字
    1. break
    2. continue

goto语句

goto语句用于无条件跳转,可以无条件地转移到程序中指定的行;它通过标签进行代码间的无条件跳转。

goto后面接一个标签,这个标签的意义是告诉Go程序下一步要执行哪行的代码,

格式

go 复制代码
goto 标签;
...
...
标签: 表达式;

例子

go 复制代码
import "fmt"

func main() {

    goto flag
    fmt.Println("B")
flag:
    fmt.Println("A")

}
css 复制代码
执行结果,并不会输出 B ,而只会输出 A

构成循环

go 复制代码
import "fmt"

func main() {
    i := 1
 flag:
    if i <= 5 {
        fmt.Println(i)
        i++
        goto flag
    }
}

goto语句与标签之间不能有变量声明,否则编译错误。

接口

**接口(interface)**定义了一个对象的行为规范,只定义规范不实现,由具体的对象来实现规范的细节。

格式

go 复制代码
type 接口类型名 interface{
  方法名1( 参数列表1 ) 返回值列表1
  方法名2( 参数列表2 ) 返回值列表2
  ...
}

我们来定义一个Sayer接口:

go 复制代码
// Sayer 接口
type Sayer interface {
    say()
}

定义dog和cat两个结构体:

go 复制代码
type dog struct {}

type cat struct {}

因为Sayer接口里只有一个say方法,所以我们只需要给dog和cat 分别实现say方法就可以实现Sayer接口了。

go 复制代码
// dog实现了Sayer接口
func (d dog) say() {
    fmt.Println("汪汪汪")
}

// cat实现了Sayer接口
func (c cat) say() {
    fmt.Println("喵喵喵")
}

接口的实现就是这么简单,只要实现了接口中的所有方法,就实现了这个接口。

接口类型变量

接口类型变量能够存储所有实现了该接口的实例。 例如上面的示例中,Sayer类型的变量能够存储dog和cat类型的变量。

go 复制代码
func main() {
    var x Sayer // 声明一个Sayer类型的变量x
    a := cat{}  // 实例化一个cat
    b := dog{}  // 实例化一个dog
    x = a       // 可以把cat实例直接赋值给x
    x.say()     // 喵喵喵
    x = b       // 可以把dog实例直接赋值给x
    x.say()     // 汪汪汪
}

值接收者实现接口 接口类型变量 可以接受,普通结构体类型和指针结构体类型的赋值操作

go 复制代码
// 值接收者实现接口
func (d dog) move() {
    fmt.Println("狗会动")
}

指针接收者实现接口 接口类型变量 只能接受,指针结构体类型的赋值操作

go 复制代码
func (d *dog) move() {
    fmt.Println("狗会动")
}

空接口

go 复制代码
var i interface{}
fmt.Println("类型:%T,值:%v\n", i, i) // 类型:<nil>,值:<nil>

i = 123
i = "123"
i = true
i = 3.14

可以借助这一特性,使上面的 i变量可以接受任何类型的赋值操作 ,反之拥有interface{}类型的变量不能赋值给其他类型

类型断言

判断空接口是什么类型,使用x.(T)语法

  • x:表示类型为interface{}的变量
  • T:表示断言x可能是的类型。

例子

go 复制代码
func main() {
    var x interface{}
    x = "zhangsan"
    v, ok := x.(string)
    if ok {
        fmt.Println(v)
    } else {
        fmt.Println("类型断言失败")
    }
}

返回两个参数 ,第一个v是实际值,第二个ok是布尔值,意为当前x的值是否为string类型

协程

协程(Coroutine) 是一种强大的程序设计结构 ,它允许多个任务在单个线程 ^1^内并发 ^2^执行,通过协作式的任务切换来提高程序的性能和响应性。在Go语言中,协程被称为Goroutines,是语言层面的原生支持,使得并发编程变得异常简单和高效。

一个线程上可以跑多个协程,协程是轻量级的线程。

go语言中的main函数称为主协程,可以将协程理解为一个主协程中的多个子协程,主协程结束,子协程函数跟着结束

Goroutine是Go运行时管理的轻量级线程

如何开启goroutine

调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。

例子

go 复制代码
package main

import (
  "fmt"
  "time"
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
}

func main() {
  go sing()
  go sing()
  go sing()
  go sing()
  time.Sleep(2 * time.Second)
}

如果我把这个主线程中的延时去掉之后,你会发现程序没有任何输出就结束了

这是为什么呢

那是因为主线程结束协程自动结束,主线程不会等待协程的结束

WaitGroup

我们只需要让主线程等待协程就可以了,它的用法是这样的

go 复制代码
package main

import (
  "fmt"
  "sync"
  "time"
)

var (
  wait = sync.WaitGroup{}
)

func sing() {
  fmt.Println("唱歌")
  time.Sleep(1 * time.Second)
  fmt.Println("唱歌结束")
  wait.Done()
}

func main() {
  wait.Add(4) // 不能为负数
  go sing()
  go sing()
  go sing()
  go sing()
  wait.Wait()
  fmt.Println("主线程结束")
}

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

channel(通道) 是golang在goroutine之间的通讯方式

通道格式

go 复制代码
var 变量 chan 元素类型

创建channel

通道是引用类型,通道类型的空值是nil

声明的通道后需要使用 make(chan 元素类型, [缓冲大小])函数初始化之后才能使用。

go 复制代码
package main

import "fmt"

func main() {
	var ch chan int
	fmt.Println(ch) // <nil>
	
	// 初始化通道
	ch = make(chan int, 1) //  初始化一个 有一个缓冲位的通道
	
	// 可以用短声明简写
	ch2 := make(chan bool, 2)
}

操作channel

通道有发送(send)、接收(receive)和关闭(close)三种操作

注意:发送和接收都使用<- 拼接在一起的<-操作符,只是通道变量在操作符的位置不同

  • 发送 将指定类型的值发送到此通道中
go 复制代码
package main

import "fmt"

func main() {
	// 初始化通道
	ch := make(chan int, 1) 
	ch <- 10 // 把10发送到ch中
}
  • 接收 从一个通道中接收值,获取值
go 复制代码
package main

import "fmt"

func main() {
	// 初始化通道
	ch := make(chan int, 1) 
	ch <- 10
	
	x := <- ch // 从ch中接收值并赋值给变量x
	<-ch       // 从ch中接收值,忽略结果
}
  • 关闭 我们通过调用内置的close函数来关闭通道
go 复制代码
package main

import "fmt"

func main() {
	// 初始化通道
	ch := make(chan int, 1) 
	ch <- 10
	
	x := <- ch 
	<-ch 
	
	close(ch) // 关闭当前ch通道
}

无缓冲的通道

在创建channel时使用make()创建,此方法的第二个参数不传则是无缓冲通道

无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

没有接收者,可以被编译成功,但会报一个死锁 的错误 fatal error: all goroutines are asleep - deadlock!

go 复制代码
func main() {
    ch := make(chan int)
    ch <- 10
    fmt.Println("发送成功")
    // fatal error: all goroutines are asleep - deadlock!
}

上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

go 复制代码
func recv(c chan int) {
    ret := <-c
    fmt.Println("接收成功", ret)
}
func main() {
    ch := make(chan int)
    go recv(ch) // 启用goroutine从通道接收值
    ch <- 10
    fmt.Println("发送成功")
}

有缓冲的通道

我们可以在使用make函数初始化通道的时候为其指定通道的容量 例如:

go 复制代码
func main() {
    ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
    ch <- 10
    fmt.Println("发送成功")
}

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个

单向通道

就是限制通道在函数中只能发送或只能接收

go 复制代码
func counter(out chan<- int) {
    for i := 0; i < 100; i++ {
        out <- i
    }
    close(out)
}

func squarer(out chan<- int, in <-chan int) {
    for i := range in {
        out <- i * i
    }
    close(out)
}
func printer(in <-chan int) {
    for i := range in {
        fmt.Println(i)
    }
}

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go counter(ch1)
    go squarer(ch2, ch1)
    printer(ch2)
}
go 复制代码
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。

在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的

select

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。

Go内置了select关键字,可以同时响应多个通道的操作。

select的使用类似于switch语句,下面是格式:

go 复制代码
select {
    case <-chan1:
       // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
       // 如果成功向chan2写入数据,则进行该case处理语句
    default:
       // 如果上面都没有成功,则进入default处理流程
}

并发安全和锁

当进行并发协程同时操作一个资源时,导致两者出现竞争,最后输出结果不一,称为并发不安全

这是为什么呢?

根本原因是CPU的调度方法为抢占式执行,随机调度

怎样才能做到并发安全呢?可以使用锁(Lock) 的方式

举个并发不安全的例子:

go 复制代码
package main

import (
  "fmt"
  "sync"
)

var num int
var wait sync.WaitGroup

func add() {
  for i := 0; i < 1000000; i++ {
    num++
  }
  wait.Done()
}
func reduce() {
  for i := 0; i < 1000000; i++ {
    num--
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num) // ?

}

可以看出两个协程函数add、reducenum同时去加减操作,正常情况下结果应该为 0,但是每次运行结果都不一样。

互斥锁

Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题

go 复制代码
package main

import (
  "fmt"
  "sync"
)

var num int
var wait  sync.WaitGroup
var lock  sync.Mutex

func add() {
  // 谁先抢到了这把锁,谁就把它锁上,一旦锁上,其他的线程就只能等着
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num++
  }
  lock.Unlock()
  wait.Done()
}
func reduce() {
  lock.Lock()
  for i := 0; i < 1000000; i++ {
    num--
  }
  lock.Unlock()
  wait.Done()
}

func main() {
  wait.Add(2)
  go add()
  go reduce()
  wait.Wait()
  fmt.Println(num)
}

读写互斥锁

当我们读取一个资源时没有对资源进行写操作,不需要加锁;反之,有读取也有写入操作时可以使用读写互斥锁

读写锁适合读多写少的场景

go 复制代码
package main
 
import (
	"fmt"
	"sync"
	"time"
)
 
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
var rwlock sync.RWMutex
 
func write() {
	defer wg.Done()
	// lock.Lock() //加互斥锁
	rwlock.Lock() //加写锁
	x++
	time.Sleep(time.Microsecond * 10)
	rwlock.Unlock() //解写锁
	// lock.Unlock() //解互斥锁
}
 
func read() {
	defer wg.Done()
	// lock.Lock()  //加互斥锁
	rwlock.RLock() //加读锁
	time.Sleep(time.Millisecond)
	rwlock.RUnlock() //解读锁
	// lock.Unlock() //解互斥锁
}
 
func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

线程安全下的map

如果我们在一个协程函数下,读写map就会引发一个错误

concurrent map read and map write

希望大家见到这个错误,就能知道,这个就是map的线程安全错误

Go语言的sync包中提供了一个开箱即用的并发安全版map------sync.Map

sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

go 复制代码
package main

import (
  "fmt"
  "sync"
  "time"
)

var wait sync.WaitGroup
var mp = sync.Map{}

func reader() {
  for {

    fmt.Println(mp.Load("time"))
  }
  wait.Done()
}
func writer() {
  for {
    mp.Store("time", time.Now().Format("15:04:05"))
  }
  wait.Done()
}

func main() {
  wait.Add(2)
  go reader()
  go writer()
  wait.Wait()
}

内置函数

new

new是一个内置的函数,用于分配内存

*函数签名:func new(Type) Type

  1. Type表示类型,new函数只接受一个参数,这个参数是一个类型
  2. *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
go 复制代码
func main() {
    var a *int
    a = new(int)
    *a = 10
    fmt.Println(*a) // 10
}

make

make也是用于内存分配的,区别于new,它只用于**切片(slice)、map以及通道(channel)**的内存创建,它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了

函数签名:func make(t Type, size ...IntegerType) Type

go 复制代码
func main() {
    var b map[string]int
    b = make(map[string]int, 10)
    b["测试"] = 100
    fmt.Println(b) // map[测试:100]
}

new与make的区别

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

常用库解析

fmt

用于格式化输出文本

  1. %v:通用类型占位符。可以表示任意值的类型。
  2. %d:10进制整数
  3. %f:浮点数
  4. %s:字符串
  5. %t:布尔值
  6. %c:字符(Unicode码点)
  7. %p:指针地址
  8. %e/%E:科学计数法
  9. %b:二进制整数
  10. %o:八进制整数
  11. %x/%X:十六进制整数
  12. %U:Unicode格式,表示为U+十六进制数
  13. %T:打印值的类型

Go Modules

Go Module(go mod)是Go语言官方依赖管理工具,从Go 1.11版本开始引入,取代GOPATH模式,用于管理项目依赖、版本控制和模块发布。 ‌

初始化模块

在项目根目录下终端执行以下命令来初始化一个新的模块:

bash 复制代码
go mod init 项目名

# 例如
go mod init myproject

添加依赖

当你在代码中导入外部包并运行go buildgo run时,Go会自动下载依赖并记录到go.mod文件中。

bash 复制代码
go get 包名

# 例如
go get github.com/gin-gonic/gin # 这会下载最新版本

# 指定版本可用
go get github.com/gin-gonic/gin@v1.9.1

依赖管理常用命令

日常开发中常用的go mod命令包括:

  • go mod tidy:清理未使用的依赖,补全缺失的依赖
  • go mod download :下载所有go.mod中的依赖
  • go mod vendor :将依赖复制到vendor/目录(可选)
  • go mod verify:验证依赖是否被篡改
  • go list -m all:列出当前模块的所有依赖
  • go list -m -u all:检查依赖是否有新版本

到这里就结束了,后续还会更新 go 系列相关,还请持续关注! 感谢阅读,若有错误可以在下方评论区留言哦!!!

Footnotes

  1. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

  2. 多线程程序在一个核的cpu上运行,就是并发 。 多线程程序在多个核的cpu上运行,就是并行

相关推荐
XiaoYu200236 分钟前
第7章 Prisma入门
javascript·后端
银迢迢36 分钟前
springboot的拦截器配置不拦截路径没有生效
java·后端·spring
superman超哥37 分钟前
Rust 引用的作用域与Non-Lexical Lifetimes(NLL):生命周期的精确革命
开发语言·后端·rust·生命周期·编程语言·rust引用的作用域·rust nll
独自破碎E1 小时前
Spring Bean一共有几种作用域?
java·后端·spring
古城小栈1 小时前
Rust 生命周期,三巨头之一
开发语言·后端·rust
Undoom1 小时前
0基础如何搭建个人博客?GMSSH可视化运维工具配合WordPress部署全流程教学
后端
JavaGuru_LiuYu1 小时前
Spring Boot 整合原生 WebSocket
spring boot·后端·websocket·即使通信
羊小猪~~1 小时前
数据库学习笔记(十九)--C/C++调用MYSQL接口
数据库·笔记·后端·sql·学习·mysql·考研
GetcharZp1 小时前
C++ 程序员一定要会的 RPC 框架:gRPC 从原理到实战,一次写通服务端和客户端
c++·后端·grpc
刘一说1 小时前
2026年Java技术栈全景图:从Web容器到云原生的深度选型指南(附避坑指南)
java·前端·spring boot·后端·云原生·tomcat·mybatis