Skip to content
返回

Go语言基础

1

目录

点击展开

Go语言基础

认识

Go(又称Golang) 由Google开发,于2009年首次公开发布。它旨在提供简洁、高效、可靠的软件开发解决方案。

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

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

下载安装

官网: https://go.dev/(英) 可访问 中国镜像站 https://golang.google.cn/

安装教程参考:https://learn.microsoft.com/zh-cn/azure/developer/go/configure-visual-studio-code

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

Go语言声明

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 语言中预先保留的单词,在程序中有特殊含义,不能用来定义变量或常量名字。

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

数据类型

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

基本数据类型

整型

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

提示!

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

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

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

![[../../../assets/images/2025/Pasted image 20250819113643.png]]

var num8 uint8 = 128
var num16 uint16 = 32768
var num32 uint32 = math.MaxUint32
var num64 uint64 = math.MaxUint64

浮点型

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

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

var num1 float32 = math.MaxFloat32
var num2 float64 = math.MaxFloat64

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

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

布尔值

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

var d bool = true
f := false

注意        布尔类型变量的默认值为false。
       Go 语言中不允许将整型强制转换为布尔型.
       布尔型无法参与数值运算,也无法与其他类型进行转换。

字符串

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

s1 := "hello"
s2 := "你好"

复合数据类型

数组

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

  • 数组定义:var a [len]int,比如:var a [5]int,一旦定义,长度不能变。
  • 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1
  • 数组是值类型赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。
// 数组初始化
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()函数求容量

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内置函数操作切片(切片追加)
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 类型的变量。指针变量最大的特点就是存储的某个实际变量的内存地址,通过记录某个变量的地址,从而间接的操作该变量。 ![[../../../assets/images/2025/Pasted image 20250826104221.png]]

指针声明获取格式

  • var 变量名 *类型 = new(类型)
  • 填入值:*变量名 = 值
  • 获取指针:&变量名
  • 获取值:*变量名
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

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类型

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关键字进行实现

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

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

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

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。

定义格式

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

解析

例子

//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关键字定义 格式

func 函数名(参数列表, ...) 返回值类型 {
	函数体
}

参数解析

例子

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, argsint) int {    //1个或多个int参数
}

func add2(a int, b int, argsint) 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,其他包无法访问

匿名函数

格式

func (参数列表, ...) 返回值类型 {
	函数体
}

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

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
}

闭包、递归

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

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
}

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

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 语句后面跟着的函数会延迟到当前函数执行完后再执行。

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

package main

import "fmt"

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

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

注意点:

包(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 的包

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中引入这个包

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函数此函数没有参数也没有返回值 ,充当每个包的生命周期初始化,不能在代码中主动调用它。

编译执行顺序⬇️ ![[../../../assets/images/2025/Pasted image 20251030172126.png]]

![[../../../assets/images/2025/Pasted image 20251030172239.png]]

流程控制

条件语句

格式

if 布尔表达式 {

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

例子

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相似

格式

switch var1 {
  case val1:
  ...
  case val2:
  ...
  default:
  ...
}

例子

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

以下是常用的几种格式

格式

for initialisation; condition; post {

code } // for 接一个条件表达式 for condition { code } // for 接一个 range 表达式 for range_expression { code } // for 不接表达式 for { code }

例子

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
	}
}

goto语句

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

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

格式

goto 标签;

… … 标签: 表达式;

例子

import "fmt"

func main() {

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

}
执行结果,并不会输出 B ,而只会输出 A

构成循环

import "fmt"

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

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

接口

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

格式

type 接口类型名 interface{
  方法名1( 参数列表1 ) 返回值列表1
  方法名2( 参数列表2 ) 返回值列表2

}

我们来定义一个Sayer接口:

// Sayer 接口
type Sayer interface {
    say()
}

定义dog和cat两个结构体:

type dog struct {}

type cat struct {}

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

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

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

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

接口类型变量

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

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()     // 汪汪汪
}

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

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

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

func (d *dog) move() {
    fmt.Println("狗会动")
}

空接口

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)语法

例子

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。

例子

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

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

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之间的通讯方式

通道格式

var 变量 chan 元素类型

创建channel

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

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

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)三种操作

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

package main

import "fmt"

func main() {
	// 初始化通道
	ch := make(chan int, 1) 
	ch <- 10 // 把10发送到ch中
}
package main

import "fmt"

func main() {
	// 初始化通道
	ch := make(chan int, 1) 
	ch <- 10
	
	x := <- ch // 从ch中接收值并赋值给变量x
	<-ch       // 从ch中接收值,忽略结果
}
package main

import "fmt"

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

无缓冲的通道

在创建channel时使用make()创建,此方法的第二个参数不传则是无缓冲通道 ![[../../../assets/images/2025/Pasted image 20251119100453.png]]

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

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

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

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

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

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

有缓冲的通道

![[../../../assets/images/2025/Pasted image 20251119102557.png]]

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

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

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

单向通道

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

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)
}
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。

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

select

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

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

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

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

内置函数

new

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

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

  1. Type表示类型,new函数只接受一个参数,这个参数是一个类型
  2. *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
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

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管理依赖项的方式。 模块是一组被发布、有版本号并一起分发的软件包。块可以直接从版本控制存储库或模块代理服务器下载。 有关模块的系列教程,请参阅https://golang.google.cn/doc/tutorial/create-module 默认情况下,go命令可能会从 https://proxy.golang.org 下载模块。它可能会使用位于 https://sum.golang.org 的校验码数据库来验证模块。这两个服务均由谷歌的 Go 团队运营。这些服务的隐私政策分别可在 https://proxy.golang.org/privacyhttps://sum.golang.org/privacy 找到。

Footnotes

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

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


Share this post on:

下一篇文章
2025-08-21面试