Go 语言简单入门 | 豆包MarsCode AI 刷题

324 阅读54分钟

#编程语言 #go

1.什么是go语言(google出品)

  1. 高性能,高并发(标准库或者基于标准库的第三方库就支持高并发,不像其他语言需要找高度性能优化过的三方库)
  2. 语法简单,类似C语言,但是还简化了
  3. 丰富的标准库,稳定性,兼容性好
  4. 完善的工具链
  5. 静态编译
  6. 快速编译
  7. 跨平台
  8. 垃圾回收
  • 每个可执行代码都要包含package import function三个部分
  • Golang以包来管理代码

2 基本语法

  • 本文适合有一定编程基础的观看
  • 虽然说是入门,但也不是很入门,有一些细节,但是看不懂的话也不必纠结(可能是本人水平不够没法讲好或者读者基础不够),也会省略一些常见的基础细节(懒得讲了,默认你有一定基础)
package main

import (
    "fmt"
)

func main() {
    fmt.Println("hello world")
}
  • package main 表示该程序属于main这个入口包
  • import导入了标准库fmt,这个包主要用于往屏幕输入输出字符串,格式化字符串
    • 一句import语句可以接一个圆括号,包含多个包;也可以直接只导入一个包。建议两者混合使用,分组导入。
    • go中import导入的是目录而不是包名,因此go不强制要求包名和目录名一致。但是按照约定,习惯,包名一般就是源文件所在目录的最后一级目录一致。
    • 在代码引用包内的成员,用的是包名而不是目录名
    • 一个文件夹内只能存在一个包名,源文件名称无限制
    • 多个文件夹下有相同名字的包,它们是彼此无关的
    • 在 Go 中,如果一个名字以大写字母开头,那么它就是已导出的。例如,Pizza 就是个已导出名,Pi 也同样,它导出自 math 包,在导入一个包时,你只能引用其中已导出的名字。 任何「未导出」的名字在该包外均无法访问。类似于java的public 和private
  • go run xxx.go来直接运行程序。当然这个go文件里面要有主函数,这个跟C一样。但是一个项目一般只能有一个主函数。不像java可以每个类都有一个主函数
  • go build xxx.go构建程序得到一个二进制文件,可以直接运行
  • 每个语句后面没有分号,你加了编译器也会给你去掉

2.1 变量

var

  • 使用var来自动推导变量类型。
  • 在任何时候,创建一个变量并初始化为零值,习惯上会使用关键字 var。这种用法是为了更明确地表示一个变量被设置为零值。
  • 可以在包级别声明变量,即全局变量
  • 可以后面接一个圆括号,一次性声明多个变量

短变量声明 :=

  • 使用:=来定义变量,一个短变量声明操作符在一次操作中完成两件事情

    • 声明一个变量
    • 初始化。
  • 短变量声明操作符会根据右侧给出的右值(初始值)自动推导变量类型。

  • 使用场景:

    • 快速声明局部变量:当你需要快速声明一个局部变量并且立即赋值。
    • 变量重新赋值:如果一个变量名在当前作用域内已经声明过了,再次使用短变量声明符会重新赋值,而不是重新声明一个
  • 使用限制

    • 不能在函数外面使用,即不能用来声明全局变量。
    • 短变量声明操作符左边至少得有一个变量是没有定义过的。
    • 变量声明可以包含初始值,每个变量对应一个。
    • 如果提供了初始值,则类型可以省略;变量会从初始值中推断出类型。
var a ="fwda"
var d =true
var c float64
var b,c int = 1, 2
var g, h = 1 ,2
e ,f := 4,5
  • 如果定义的时就赋予初始值,是可以省略变量的类型的。

基本类型

//布尔类型
bool 

//字符串类型
string

//整数类型
int  int8  int16  int32  int64
uint uint8 uint16 uint32 uint64 uintptr

//uint8的别名,即一个字节
byte 

//int32的别名,表示一个Unicode字符,常用来表示单个字符
rune

//浮点类型
float32 float64

//复数类型
complex64 complex128
  • intuint 和 uintptr 类型在 32位系统上通常为 32位宽,在 64位系统上则为 64位宽。
  • 一般情况下,整数类型选择int类型,除非你有明确的需要或者特殊的理由,选择无符号整数类型或者其他整数类型。

零值(变量默认初始化)

  • 没有明确初始化的变量声明会被赋予对应类型的 零值
    • 数值类型为 0
    • 布尔类型为 false
    • 字符串为 ""(空字符串)

类型转换

  • 与C不同,Go在不同类型的变量间复制需要显示转换。否则会编译错误。这样也有利于统一规范,减少BUG和编程细节,提升代码可读性。
  • 表达式 T(v) 将值 v 转换为类型 T

全局变量

  • 全局变量如果要在包外访问,首字母要大写
  • 大写字母开头的变量包外可见,小写字母开头的变量仅包内可用

常量

  • 使用const来声明常量,并且可以根据上下文自动确定常量类型(也可以自己显式指定类型)
  • 不能使用:=

匿名变量

  • _变量,是系统保留的匿名变量,赋值后自动释放
  • 常用作变量占位符,如果一个函数返回多个变量,我们只需要其中一个,那么其他的都可以用这个占位符

2.2 指针类型

  • go支持指针,但是功能上比C指针弱很多,相当于C指针的子集。比如没有指针运算
  • 主要用途是对传入的参数进行修改
func add2(n int) {
    n += 2
}

func add2ptr(n *int) {
    *n += 2
}

func main() {

    n := 5
    add2(n)
    fmt.Println(n) // 5

    add2ptr(&n)
    fmt.Println(n) // 7
}
  • new是一个内部函数,new(T)操作相当于给一个数据类型T分配一个内存,然后返回指向这个数据的指针,并且给它默认初始化(赋予零值)

  • 表达式 new(Type) 和 &Type{} 是等价的。 都是返回一个指向Type类型的指针

  • 使用 new 函数时,声明变量和分配内存并不需要放在一起。可以先声明一个变量,然后再通过 new 函数为之分配内存

var nick *Person  
nick = new (Person)

2.3 条件判断

  • if-else 语句的条件判断没有小括号,即使添加了括号,编译器也会给你去掉
  • 允许在判断条件之前执行一个简单的语句,用;隔开,一般用于声明临时变量之类,在 if 的简短语句中声明的变量同样可以在对应的任何 else 块中使用。
  • if后面必须是大括号,不能把if语句写到同一行
    • if v>10 { dosth() } 这就是非法的
    • if v > 10 dosth() 这也是非法的,必须要有大括号

2.4 循环

  • go只有for循环
  • 但是最基本的for循环可以有三个循环块:
    • 循环变量块
    • 循环条件块
    • 循环变量变化快
  • 根据对这三个块的调整(去掉),可以代替其他语言的while循环。(其他语言也可以吧,如C和java,相信很多人都写过)
  • continuebreak语句和其他语言一样,而且也可以指定重新的循环是哪个,跳出到哪个循环,即加label。但是这个语法特性显然用的不多,也不建议使用,就不介绍了。
for{
   这是一个死循环
}

for j := 7; j<9;++j{
	continue;
	break;
}

i :=1 
for i<=3{
	++i
}

2.5 switch语句

  • switch语句后面不需要括号
  • switch里面的每个分支结尾自带break,一个分支没有以break结束(所以不需要你写break语句)不会进入下一个分支,直接跳过出witch语句。
  • switch的条件可以是任意类型,字符串,结构体,这个与其他语言不同,是GO的一个特色
  • 如果希望进入某个case后,继续顺序往下执行,可以使用fallthrough关键字

无条件 switch

  • switch语句还可以取代if-then-else语句,将一长串语句写得更清晰。
t := time.Now()
switch {
	case t.Hour<12 : //多个else if
		fmt.println("")
	default:  //相当于else
		fmt.println("")
}

2.6 数组

  • 因为go的var:=两个变量声明符,所以数组声明与其他语言有点区别。
  • 其他的和其他语言没什么区别了

数组声明

var a [5]int
a[4]=100

b := [5]int{1,2,3,4,5}
fmt.Printlb(b)

var c [2][3]int
  • :=让编译器自动推导数组类型

  • [...]可以让编译器根据我们提供的元素个数计算数组大小 arr3 := […]int{10,20,30,40,50}

  • 用具体值初始化索引为 1 和 3 的元素 指定下标来初始化部分元素
    arr4 := [5]int{1:20,3:40}

  • 用具体值初始化索引 1 和 3元素,[...]编译器会让自动以最大的索引作为数组大小 arr := [...]int{1:20,3:40}

数组是值类型

  • 在 Go 中,数组是值类型,这意味着数组可以相互直接复制。(java中的数组是引用类型)
  • 变量名代表整个数组,同类型的数组可以赋值给另一个数组。
  • 注意,数组长度和数组元素类型都是数组类型的一部分。两个数组类型相等当且仅当数组长度和数组元素类型都相同
var arr1 [3]string
arr2 := [3]string{"nick", "jack", "mark"}
// 把 arr2 的赋值(其实本质上是复制拷贝)到 arr1
arr1 = arr2
  • 这不是引用,是直接拷贝,两个数组值完全一样,但是彼此之间没有任何联系(java是直接复制一个引用,赋值后两个数组引用同一个数组)。
  • 下面操作编译器会报错
// 声明第一个包含 4 个元素的字符串数组
var arr1 [4]string
// 声明第二个包含 3 个元素的字符串数组,并初始化
arr2 := [3]string{"nick", "jack", "mark"}
// 将 arr2 赋值给 arr1 Error
arr1 = arr2

指针数组

  • 数组的元素类型除了可以是某个类型外,还可以是某个类型的指针类型。
  • 下面声明一个指针数组,然后使用 * 运算符(解引用)就可以访问元素指针所指向的值: arr := [5]*int{0: new(int), 1: new(int)}
  • 用一个指针数组赋值给相同类型的指针数组时,复制的是指针(浅拷贝),而不会创建一个系列新对象并且返回它们的指针。也就是说这两个指针数组的指针指向的是相同的底层数组
  • 用指针赋值给指针也是一样的。这些跟C一样

把数组作为参数传递给函数

  • 有学过C或者C++的就好理解这块了

  • 上面说到,go中的数组是一个值类型。所有值类型变量在赋值和作为参数传递时都将会发生一次拷贝。如果将数组直接传递给函数,那么函数每调用一次就会拷贝一次整个数组,并且在函数中也无法修改外面源数组内容,函数里操作的仅仅是源数组的一个副本。

  • 这个操作在性能和内存上都消耗很大,效率很低。

  • 高效的解决方式就是传入指向数组的指针,这样只需要复制指针了,消耗很小。

  • 每次调用函数,栈上都要创建100万个int64类型元素,消耗8MB内存

func showArray(array [1e6]int64){
    // do something
}
var arr [1e6]int64
showArray(arr)
  • 每次调用函数,栈上都要创建1个int64指针类型元素,消耗8字节内存(64位系统的一个指针是8字节,32位系统就是4字节)
func showArray(array *[1e6]int64){
    // do something
}
var arr [1e6]int64
showArray(&arr)
  • 这个方法能够更有效地利用内存,性能也更好。需要注意的是,此时在函数内外操作的都是同一个数组中的元素,会互相影响。

多维数组

// 声明一个二维整型数组
var arr [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
arr1 := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 和 3 的元素
arr2 := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
arr3 := [4][2]int{1: {0: 20}, 3: {1: 41}}
  • 因为每个数组都是一个值,所以可以独立复制某个维度:
// 将 arr1 的索引为 1 的维度复制到一个同类型的新数组里
var arr4 [2]int = arr1[1]
// 将外层数组的索引为 1、内层数组的索引为 0 的整型值复制到新的整型变量里
var value int = arr1[1][0]

小结

  • 数组在 Golang 中是作为高性能的基础类型设计的,功能也比较少,因此对用户来说使用起来并不是特别方便。
  • 这一点在众多的开源代码中(数组用的少,slice 用的多)可以得到印证。
  • 但是很多复杂数据类型都是基于数组实现的,底层都是一个数组类型,比如下面的slice切片。实际开发还是建议使用slice吧。

2.7 切片

  • 可变长度的数组,任意时刻可以修改长度。相当于其他语言的vector

切片创建和初始化

使用make()函数创建切片
  • 创建一个整型切片, 长度和容量都是 5 个元素 a := make([]int, 5)

  • 创建一个整型切片,其长度为 3 个元素,容量为 5 个元素 b := make([]int, 3, 5)

    • 创建一个整型切片,使其长度大于容量。这是非法的,会编译错误 c := make([]int, 5, 3)
初始化切片
  • 使用字面量直接初始化,与创建数组类似,但是不需要指定[]运算符里面的值。初始的长度和容量会与我们提供的元素个数一致

  • 创建字符串切片,其长度和容量都是 3 个元素 myStr := []string{"Jack", "Mark", "Nick"}

  • 创建一个整型切片,其长度和容量都是 4 个元素 myNum := []int{10, 20, 30, 40}

  • 当使用切片字面量创建切片时,其实还可以设置初始长度和容量。

  • 在初始化时给出所需的长度和容量作为切片的最后一个索引

  • 创建一个长度和容量都是100的字符串切片 a := []string{99:""}

  • 区分数组的声明和切片的声明方式

  • 当使用字面量来声明切片时,其语法与使用字面量声明数组非常相似。

  • 二者的区别是:如果在 [] 运算符里指定了一个值,那么创建的就是数组而不是切片。只有在 [] 中不指定值的时候,创建的才是切片。

// 创建有 3 个元素的整型数组
myArray := [3]int{10, 20, 30}
// 创建长度和容量都是 3 的整型切片
mySlice := []int{10, 20, 30}

nil与空切片

nil切片
  • 定义:一个声明但未初始化的切片变量会自动设置为nilnil切片实际上是指向底层数组的指针为nil的切片,长度和容量都为0。
  • 使用:尝试遍历或访问nil切片的元素会导致运行时错误(panic),因为此时并没有分配任何内存空间给这个切片,也可认为这是空指针错误。
  • 创建 nil 整型切片 var myNum []int

  • nil 切片可以用于很多标准库和内置函数。在需要描述一个不存在的切片时,nil 切片会很好用。比如,函数要求返回一个切片但是发生异常的时候。

空切片
  • 定义:一个已经初始化但不包含任何元素的切片被称为空切片。可以通过多种方式创建,如 var s []int = []int{} 或者 s := make([]int, 0)。空切片的指向底层数组的指针不是空指针,是指向一个空数组。
  • 使用:可以安全地对空切片执行迭代、追加等操作,因为它已经分配了底层的数组。
  • 使用 make 创建空的整型切片 myNum := make([]int, 0)

  • 使用切片字面量创建空的整型切片 myNum := []int{}

  • 空切片的底层数组中包含 0 个元素,也没有分配任何存储空间。想表示空集合时空切片很有用。比如数据查询返回0个查询结果的时候(你要用nil切片也可以,但是这也是约定

通过切片创建新的切片

  • 切片之所以被称为切片,是因为通过给一个切片进行切片,得到一个子切片,也就是把底层数组切出一部分。
slice[i:j]
slice[i:j:k]
  • slice[i:] 从i切到末尾

  • slice[:j] 从开头切到j(不含j)

  • slice 从头切到尾,等价于复制整个slice

  • 注意:子切片的长度就是你切出来的切片的长度,但是容量却是你子切片的起点到父切片结尾的长度。

  • 子片的长度是切片操作符指定的范围,而容量是从子片的起始位置到父切片底层数组的末尾。

 s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}  
//输出切片的长度和容量  
fmt.Printf("len(s)=%d cap(s)=%d\n", len(s), cap(s))// 10 10  
  
// 不同方式的切片  
s1 := s[2:5]//起点是2,终点是5  
s2 := s[:4]//起点是0,终点是4  
s3 := s[3:]//起点是3,终点是10  
s4 := s[:]//起点是0,终点是10  
  
// 输出这些切片的长度和容量  
fmt.Printf("s1: len=%d cap=%d\n", len(s1), cap(s1))// 3 8  
fmt.Printf("s2: len=%d cap=%d\n", len(s2), cap(s2))// 4 10  
fmt.Printf("s3: len=%d cap=%d\n", len(s3), cap(s3))// 7 7  
fmt.Printf("s4: len=%d cap=%d\n", len(s4), cap(s4))// 10 10

原理和使用需要注意的问题

  • 切片slice原理是它存储了一个长度和容量,还有一个指向数组的指针。
  • 不支持py那样的负数索引
  • 对一个slice进行切片得到子切片,子切片的底层是定义了一个新指针指向父切片的某个位置作为子切片的起点,而不是拷贝。
  • [[高质量编程与性能调优实战#^sliceLargeMemoryUnfreeTrap]]

切片扩容

扩容时会容量不够的话就会扩容并且返回新的slice

  • 相对于数组而言,使用切片的一个好处是:可以按需增加切片的容量,对内存利用率高。

  • 使用Golang 内置的 append() 函数扩容。

  • 使用 append() 函数,需要一个被操作的切片和一个要追加的值,当 append() 函数返回时,会返回一个包含修改结果的新切片,一般需要将这个结果赋予给原切片,即a =append(a, value)

  • 函数 append() 总是会增加新切片的长度。容量则不一定改变,当容量不足时,会进行底层数组扩容操作

  • 如果切片的底层数组没有足够的可用容量,append() 函数会创建一个新的足够大的底层数组,即增长容量,将被引用的现有的底层数组复制到新数组里,再追加新的值。此时 append 操作同时增加切片的长度和容量。

  • 函数 append() 会智能地处理底层数组的容量增长。底层有一套增长算法,初始时容量小,容量增长快,可能是倍增。当容量足够大的时候,这个增长因子可能就比2小了,应该是1.25或者1.5。具体数值自己查或者看源码吧。


  • 上面提到,子切片的容量并不一定等于长度,有可能容量大于长度。
  • 当子片扩容时,如果新容量小于或等于父切片的剩余容量,子片会继续使用父切片的底层数组;如果新容量大于父切片的剩余容量,子片会分配一个新的底层数组,这样就不会影响到父切片。
  1. 子切片只增加了一个(少量)元素,需要的容量还是足够的,父切片还能装得下,因此父切片被修改了
// 创建一个父切片  
parent := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}  
fmt.Printf("parent: %v\n", parent) //parent: [1 2 3 4 5 6 7 8 9 10]  
  
// 从父切片中切出一个子片,长度为3,容量为8  
child := parent[2:5]  
fmt.Printf("child: %v\n", child)                             // child: [3 4 5]  
fmt.Printf("child: len=%d cap=%d\n", len(child), cap(child)) // 输出: len=3 cap=8  
fmt.Println()  
  
// 向子片中追加元素,触发扩容  
child = append(child, 11111)  
fmt.Printf("child: %v\n", child)                                          //child: [3 4 5 11111]  
fmt.Printf("child after append: len=%d cap=%d\n", len(child), cap(child)) // 输出: len=9 cap=16  
  
fmt.Printf("parent: %v\n", parent) // parent: [1 2 3 4 5 11111 7 8 9 10]  
  
// 修改子片的元素,影响到父切片  
child[0] = 99  
fmt.Printf("child: %v\n", child)   //[99 4 5 11111 ]  
fmt.Printf("parent: %v\n", parent) //[1 2 99 4 5 11111 7 8 9 10]
  1. 子切片一次性增加了过多元素,父切片剩下的容量也不够了,子切片就重新分配了一个更大的底层数组,跟父切片没有关系了,因此子切片扩容影响不到父切片。
// 创建一个父切片  
parent := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}  
fmt.Printf("parent: %v\n", parent) //parent: [1 2 3 4 5 6 7 8 9 10]  
  
// 从父切片中切出一个子片,长度为3,容量为8  
child := parent[2:5]  
fmt.Printf("child: %v\n", child)                             // child: [3 4 5]  
fmt.Printf("child: len=%d cap=%d\n", len(child), cap(child)) // 输出: len=3 cap=8  
fmt.Println()  
  
// 向子片中追加元素,触发扩容  
child = append(child, 11111, 22222, 333333, 44444, 55555, 66666, 7777)  
fmt.Printf("child: %v\n", child)                                          //[3 4 5 11111 22222 333333 44444 55555 66666 7777]  
fmt.Printf("child after append: len=%d cap=%d\n", len(child), cap(child)) // len=10 cap=16  
  
fmt.Printf("parent: %v\n", parent) //[1 2 3 4 5 6 7 8 9 10]  
  
// 修改子片的元素,不再影响到父切片  
child[0] = 99  
fmt.Printf("child: %v\n", child) //[99 4 5 11111 22222 333333 44444 55555 66666 7777]  
fmt.Printf("parent: %v\n", parent) //[1 2 3 4 5 6 7 8 9 10]
  • 怎么避免这个问题呢,下面的小节会介绍到

将一个切片追加到另一个切片

  • 内置函数 append() 也是一个可变参数的函数。这意味着可以在一次调用中传递多个值。如果使用 … 运算符,可以将一个切片的所有元素追加到另一个切片里:
 // 创建两个切片  
    slice1 := []int{1, 2, 3}  
    slice2 := []int{4, 5, 6}  
  
    // 将 slice2 添加到 slice1   
    slice1 = append(slice1, slice2...)  
    fmt.Printf("slice1: %v\n", slice1) // 输出: slice1: [1 2 3 4 5 6]  
}

限制切片的容量

  • 在创建子切片时,可以使用第三个索引选项引可以用来控制新切片的容量。

  • 其目的并不是要增加容量,而是要限制容量。

  • 允许限制新切片的容量为底层数组提供了一定的保护,可以更好地控制追加操作。

  • slice[i:j:k] 生成一个具有指定容量的子切片。

  • 新切片是原切片的索引[i,j)[i,j),长度是jij-i容量是kik-i

  • append() 在操作切片时会首先使用可用容量。一旦没有可用容量,就会分配一个新的底层数组。

  • 这导致很容易忘记切片间正在共享同一个底层数组。一旦发生这种情况,对切片进行修改,很可能会奇怪的而难以排查修复的问题。如果在创建切片时设置切片的容量和长度一样,就可以强制让新切片的第一个 append 操作创建新的底层数组,与原有的底层数组分离。这样就可以安全地进行后续的修改操作了。

  • 常用 于需要精确控制子切片容量的场景,如在多 goroutine 中共享切片时,防止一个 goroutine 修改切片容量影响其他 goroutine。

  • slice[i:j] 通常用于普通的切片操作,适用于大多数情况。

  • 如果设置的子切片容量比可用的容量还大,就会得到运行时错误

遍历切片

用普通for和rangefor咯 [[go语言入门#2.9 range]]

切片的拷贝

  • Golang 内置的 copy() 函数可以将一个切片中的元素拷贝到另一个切片中
  • func copy(dst, src []Type) int 表示把切片src中的元素拷贝到切片dst,并且返回拷贝成功的元素个数。
    • 如果src比dest长,就截断,只拷贝dest切片长度个元素
    • 反之只拷贝src里面的元素
// 创建源切片  
src := []int{1, 2, 3, 4, 5}  
  
// 创建目标切片,长度等于源切片  
dest1 := make([]int, len(src))  
copy(dest1, src)  
fmt.Printf("dest1: %v\n", dest1) // 输出: dest1: [1 2 3 4 5]  
  
// 创建目标切片,长度小于源切片  
dest2 := make([]int, 3)  
copy(dest2, src)  
fmt.Printf("dest2: %v\n", dest2) // 输出: dest2: [1 2 3]  
  
// 创建目标切片,长度大于源切片  
dest3 := make([]int, 7)  
copy(dest3, src)  
fmt.Printf("dest3: %v\n", dest3) // 输出: dest3: [1 2 3 4 5 0 0]

把切片传递给函数

  • 函数间传递切片就是以切片值的形式传递切片。由于切片很小(一指针,两整数类型),在函数间复制和传递切片成本很低。
  • 让我们创建一个包含 100 万个整数的切片,并将这个切片以值的方式传递给函数 foo():
// 函数 foo() 接收一个整型切片,并返回这个切片  
func foo(slice []int) []int {  
    ...  
    return slice  


s := make([]int, 1e6)  
// 将 s 传递到函数
foo()slice = foo(s)  
}
  • 在 64 位架构的机器上,一个切片需要 24 字节的内存:指针字段需要 8 字节,长度和容量字段分别需要 8 字节。

  • 由于切片包含的元素都在底层数组里面,不属于切片本身。所以将切片作为函数参数传递时,对底层切片数组没有影响。传参只会复制切片本身三个元素,不会涉及底层数组

  • 传入函数形成的切片副本和源切片都是一模一样的,包括指向底层数组的指针,因此函数里面修改切片的元素也会影响到外面的切片

  • 在函数间传递 24 字节的数据会非常快速、简单。这也是切片效率高的地方。不需要传递指针和处理复杂的语法,只需要复制切片,按想要的方式修改数据,就能修改到源切片。

// Function to add 3 to each element of the slice  
func addThree(slice []int) {  
    for i := range slice {  
       slice[i] += 3  
    }  
}  
func main() {  
    // Create a slice  
    s := []int{1, 2, 3, 4, 5}  
  
    // Pass the slice to the function  
    addThree(s)  
  
    // Print the modified slice  
    fmt.Printf("Modified slice: %v\n", s) // Output: Modified slice: [4 5 6 7 8]  
}

2.8 映射(map)

  • 映射是一种数据结构,用于存储一系列无序的键值对,它基于键来存储值。映射的特点是能够基于键快速检索数据。键就像是数组的索引一样,指向与键关联的值。
  • 与 C++、Java 等编程语言不同,在 Golang 中使用映射不需要引入任何库。因此 Golang 的映射使用起来更加方便。
  • 映射是无序集合,所以即使以同样的顺序保存键值对,迭代映射时,元素的顺序可能会不一样。无序的原因是映射的实现使用了哈希表(可以去看它源码来查看它的实现)。

创建和初始化映射

使用 make 函数声明映射
  • 创建一个映射,键的类型是 string,值的类型是 int myMap := make(map[string]int)
  • make(map[ key ] value )
使用字面量声明映射
  • 创建一个映射,键和值的类型都是 string,并使用两个键值对初始化映射 myMap := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}
  • 这个方法更常用。映射的初始长度会根据初始化时指定的键值对的数量来确定。
映射的键类型可以是任何类型
  • 映射的值类型可以是任何类型,可以是内置类型,也可以是结构体,只要这个类型能使用==运算符作比较(哈希表实现,如果是红黑树的话就需要实现比较运算符了)。

  • 切片,函数,包含切片的结构体类型等类型,都具有引用含义,不能作为映射的键。使用这些类型作为键类型会编译错误

    • 创建一个映射,使用字符串切片作为映射的键,以下会编译错误 myMap := map[[]string]int{}
  • 虽然切片不能作为映射的键,但是却可以作为映射的值。比如当我们使用一个键对应一组数据的时候,这个就很有用

    • 创建一个映射,使用字符串切片作为值(存储字符串切片的映射) myMap := map[int][]string{}
nil映射
  • 与切片类似,声明一个未初始化的映射,得到一个nil映射。来
  • nil 映射既没有键,也不能添加键(没有分配空间,使用的话会触发panic)。

查找键值(查询键值是否存在)

有两种方法可以检查键值对是否存在,第一种方式是获取键值对中的值以及一个表示这个键是否存在的布尔类型标志:

  • 法一:尝试获取键值对的值,来判断是否存在
  • 若 key 不在映射中,则 elem 是该映射元素类型的零值。
  elem, exist := m[key]
  if exist{
	  ...
  }else{
      
  }
  • 法二:直接获取,根据得到的值是否为零值来判断这个键是否存在
value := myColors["Blue"]
if value != "" {
    fmt.Println(value)
}
  • 显然,这种方式只能用在映射存储的值都是非零值的情况下。这种方法不建议。
  • 在go中,通过键来索引映射值时,即使键不存在,也会返回一个零值。

遍历映射

  • 直接用range,看下文 [[go语言入门#2.9 range]]

删除映射中的键值对

  • Golang 提供了一个内置的函数 delete() 用于删除集合中的元素
  • myMap中删除键为"hello"的键值对。 如果该键不存在,什么都不会发生,但是如果传入的键是nil,该操作会触发panic delete(myMap, "hello")

在函数间传递映射

  • 与切片类似,在函数间传递映射并不会制造出该映射的一个副本。实际上,当传递映射给一个函数,底层传递的也是一个指针类型,因此对函数中的映射进行修改,外面的实参映射也会发生修改。
  • 因此传递映射的效率很高,性能很好,开销低。

2.9 range

  • 类比于java,C++的range-for循环
  • 用于遍历slice,数组,map等数据结构
  • 关键字 range 总是会从被迭代对象头部开始遍历。如果想对遍历做更多的控制,可以使用传统的 for 循环配合 len() 函数实现。

遍历数组

  • range遍历数组会返回两个值,第一个是index,第二个是value。如果不需要其中一个可以用匿名变量_来忽略
arr := []int{1, 2, 3, 4, 5}  
//忽略索引
for _, value := range arr {  
    fmt.Println(value)  
}
  • 如果你只需要索引,忽略第二个变量即可。
arr := []int{1, 2, 3, 4, 5}  
  
// Ignore the value  
for index := range arr {  
    fmt.Println(index) // 0 1 2 3 4  
}

迭代切片

  • 当迭代切片时,关键字 range 会返回两个值。第一个值是inex,第二个值是切片该位置对应value的一份副本。
  • range 创建了每个元素的副本,而不是直接返回对该元素的引用。要想获取每个元素的地址,使用普通for循环+取地址运算符。
slice := []int{1, 2, 3, 4, 5}  
  
// 使用 range 迭代切片  
for index, value := range slice {  
    fmt.Printf("索引: %d, 值: %d\n", index, value)  
}

迭代映射

m := map[string]int{"a": 1, "b": 2, "c": 3}  
  
// 使用 range 迭代 mapfor key, value := range m {  
    fmt.Printf("键: %s, 值: %d\n", key, value)  
}

迭代字符串

str := "hello"  
  
for index, char := range str {  
    fmt.Printf("索引: %d, 字符: %c\n", index, char)

迭代通道Channel

  • 这个不是语法入门)

2.10 函数

  • go用func定义一个函数
  • 函数的参数类型,函数返回类型都是后置的
  • Golang命名推荐驼峰命名法
  • 函数名首字母小写,则代表只能在包内使用,反之则是可以在包外被使用
  • 对于多个相同类型的参数,参数类型可以只写一个
  • 函数可以返回任意多个返回值
func add(a int, b int) int {
    return a + b
}

func add2(a, b int) int {
    return a + b
}

// 函数原生支持返回多个值
// 在业务逻辑中,几乎所有函数都返回两个值,第一个是结果,第二个是错误信息
func exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}

func main() {

    res := add(1, 2)
    fmt.Println(res) // 3

    v, ok := exists(map[string]string{"a": "A"}, "a")
    fmt.Println(v, ok) // A True

}

带名字的返回值

  • 函数返回值是可以被命名的
  • 它们会被视作定义在函数顶部的变量。
  • (裸返回语句)使用空的return语句直接返回已命名的返回值
func split(sum int) (x,y int){
	x = sum*4/9
	y = sum-x
	return
}
  • 裸返回语句应当仅用在这样的短函数中。在长的函数中它们会影响代码的可读性。
  • 返回值的命名应当能反应其含义,它可以作为文档使用。

函数闭包

什么是闭包?
  • 闭包是一个函数对象,它记录了一些信息,这些信息来自于创建它的上下文环境。具体来说,当一个函数被定义在一个特定的作用域内(比如另一个函数内部),并且这个内部函数访问了外部作用域中的变量(或者说这个闭包被绑定到这些变量),那么这个内部函数就成为了一个闭包。
为什么需要闭包?
  • 闭包允许你在函数外面保存一些状态或数据,而不需要通过参数传递这些数据。这使得代码更加灵活和简洁。闭包通常用于回调函数、事件处理器等场景,这些场景下你需要记住某些信息以便在未来某个时刻使用它们。
Go函数闭包
  • Go 函数可以是一个闭包。 例如,函数 adder 返回一个闭包。每个闭包都被绑定在其各自的 sum 变量上。
func adder() func(int) int {
	sum := 0
	return func(x int) int {
		sum += x
		return sum
	}
}

func main() {
	pos, neg := adder(), adder()
	for i := 0; i < 10; i++ {
		fmt.Println(
			pos(i),
			neg(-2*i),
		)
	}
}
  • 假设我们有一个函数createCounter,它返回一个增加计数器值的函数。每次调用返回的函数时,计数器都会增加1。
func createCounter() func() int {
    count := 0 // 定义一个局部变量
    return func() int { // 返回一个匿名函数
        count++ // 访问外部函数的局部变量
        return count
    }
}

func main() {
    counter := createCounter() // 调用 createCounter 并获取返回的函数
    fmt.Println(counter()) // 输出: 1
    fmt.Println(counter()) // 输出: 2
}
  • createCounter 函数内部定义了一个变量 count,然后返回了一个匿名函数。这个匿名函数能够访问并修改 count 变量,即使 createCounter 已经执行完毕。这就是闭包的概念——函数记住了它被创建时的环境,并且可以在之后继续使用这些环境中的变量。
关于变量的作用域和生命周期
  • 在上面的例子中,count 是一个局部变量,但是由于闭包的存在,它的生命周期被延长了。即使 createCounter 函数已经执行完毕,count 变量仍然存在于内存中,因为有闭包引用了它。这种机制是实现一些高级编程技巧的基础,如记忆化、延迟计算等。
  • 我的理解是将闭包和闭包被绑定到的变量看成一个整体,或者说结构体。闭包相当于结构体方法,闭包被绑定到的变量就相当于结构体成员。
  • 下面是一个返回斐波拉契数列的闭包
func fibonacci() func() int {  
    // 初始化前两个斐波那契数  
    a, b := 0, 1  
    return func() int {  
       // 计算下一个斐波那契数  
       a, b = b, a+b  
       return a  
    }  
}  
  
func main() {  
    f := fibonacci()  
    for i := 0; i < 10; i++ {  
       fmt.Println(f()) // 打印前10个斐波那契数  
    }  
}

defer语句

  • defer 语句会将defer后面的函数调用 ,推迟到外层函数返回之后执行。 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
  • defer语句后面必须是函数调用语句,不能是其他语句,或者会CE
func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}//输出 hello world
用途
  • 文件的关闭,数据库连接的释放等函数 ,这样开启和关闭可以写到一起,让代码更加整洁,也能有效防止开发业务代码长而忘记关闭
原理
  • defer语句的函数调用的地址压入一个栈,当前函数执行完毕,就会将栈中指令地址弹出执行,然后再继续执行其他的外部函数。
  • 因此如果有多条defer语句,会依照它们在外部函数的顺序,从下往上依次执行。

2.11 结构体

  • 结构体就是带类型的字段的集合, 支持用户自定义类型
  • 在结构体中也遵循用大小写来设置公有或私有的规则。如果一个结构体名称首字母大写,那么这个结构体可以被其他包访问;否则,只能在保内进行访问。而结构体内的字段也一样,也是遵循一样的首字母大小写确定可用性的规则。
  • 结构体中的字段可以是任何类型,甚至是结构体本身,也可以是函数或者接口

字段标记

  • 在定义结构体时还可以为字段指定一个标记信息:
type Person struct {
    Name  string `json:"name"`
    Age     int  `json:"age"`
    Email string `json:"email"`
}
  • 这些标记信息通过反射接口可见,并参与结构体的类型标识,但在其他情况下被忽略。

结构体的初始化

var
  • 不需要赋值,编译器会给声明的结构体自动分配空间,里面的字段默认初始化,即赋予零值
  • 常用于仅仅声明一个变量,而不立即初始化。
  type user struct {  
    name     string  
    password string  
}
var d user  
d.name = "wang"  
d.password = "1024"
使用new函数或者直接创建指针
  • 以下创建了h,f。两个都是指向一个user结构体的指针
h := &user{name: "wang", password: "1024"} //混合字面量语法(composite literal syntax) 

f := new(user)
(*f).name="wang" // 解引用操作可以省略,编译器会隐式解引用,但是不建议这样做
使用键值对(字面值初始化)
  • 使用键值对,构造时要传入字段的初始值。并且可以省略一部分字段,没有初始化的字段就默认零值
a := user{name: "wang", password: "1024"}  
c := user{name: "wang"}  
c.password = "1024"
  • 使用字面值初始化,但是没有字段名。那么传入的值顺序很重要,要和结构体中声明字段的顺序一致    b := user{"wang", "1024"}

结构体指针

  • 对于一个指向结构体的指针p,它的结构体字段的访问方式有很多种
    • 最直接的,清晰的自然是(*p).x来访问p指针指向的结构体的字段x
    • go允许隐式解引用,使得更清晰,简洁,直接写p.x,等价于上面那句。
    • 但是不建议隐式解引用,我们还是要保持对类型的敏感的。

匿名字段

  • 结构体可以包含一个或多个匿名(内嵌)字段,这些字段没有名称,仅指明字段的类型,此时该类型就是该字段的名称。
  • 匿名字段可以是任何类型,基础类型。但是匿名字段可以是一个结构体类型,即结构体可以内嵌结构体。
  • 通过匿名字段嵌套结构体和面向对象编程中的继承概念相似,可以用来模拟类的继承行为,即go没有传统的类继承机制,而是使用结构体嵌套来实现类似继承的功能。
type test struct {
    name string
    age int
    int // 匿名字段
}

type Person struct {
    Name  string
    Age     int
    Email string
}
type Student struct {
    Person
    StudentID int
}
  • 这种简单的机制(将一个结构体内嵌入另一个结构体),使得Go很轻松的实现从一个或者多个类型中继承或全部实现它们的功能。
  • 下面是一个更为详细的例子
// 定义一个基础结构体  
type Animal struct {  
    Name string  
    Age  int  
}  
  
// 为基础结构体定义一个方法  
func (a Animal) Speak() {  
    fmt.Println("Animal speaks")  
}  
  
// 定义一个嵌套结构体,继承自 Animal  
type Dog struct {  
    Animal  
    Name string  
}  
  
// 为嵌套结构体定义一个方法  
func (d Dog) Bark() {  
    fmt.Println("Woof!")  
}  
  
func main() {  
    // 创建一个 Dog 实例  
    d := Dog{  
       Animal: Animal{Name: "Buddy", Age: 18},  
       Name:   "Golden Retriever",  
    }  
  
    // 调用继承的方法  
    d.Speak() // Animal speaks  
    // 调用 Dog 自己的方法  
    d.Bark() // Woof!  
    // 访问继承的字段  
    fmt.Println("Dog's name:", d.Age) // Dog's name: 18  
    // 访问自己的字段  
    fmt.Println("Dog's breed:", d.Name) // Dog's breed: Golden Retriever  
    // 访问继承的字段  
    fmt.Println("Animal's name:", d.Animal.Name) // Animal's name: Buddy  
}

2.12 结构体方法

  • 在Go中,结构体可以有方法,这些方法本质上是函数,但是它们与特定的接受者receiver相关联。接收器是某些类型的变量,一般是一个结构体类型。
  • 把结构体参数(接受者类型)带上括号,写到函数名前面,这样普通函数就变成了结构体方法
  • 接收者的类型定义和方法声明必须在同一包内。
  • 可以在下面的方法和变量用取地址输出下地址,看是拷贝还是引用
type Vertex struct {  
    X, Y int  
} 
  • 这是一个值接受者,调用该方法,会对调用者进行拷贝。因此方法内部对v的修改不会影响到调用者
//值接收者
func (v Vertex) test1() {  
    v.X++  
    v.Y++  
} 
  • 这是一个指针接受者,调用该方法,传递的是指向调用者的指针。因此方法内部对v的修改会影响到调用者
//指针接收者
func (v *Vertex) test2() {  
    v.X++  
    v.Y++  
}  
func main() {  
    //帮我写四个变量v1 v2 v3 v4,分别是值类型和指针类型,并且分别调用test1和test2方法,然后输出结果  
    v1 := Vertex{1, 2}  
    v1.test1()  
    println(v1.X, v1.Y)//1 2  
  
    v2 := Vertex{1, 2}  
    v2.test2()  
    println(v2.X, v2.Y)//2 3  
  
    v3 := &Vertex{1, 2}  
    v3.test1()  
    println(v3.X, v3.Y)//1 2  
  
    v4 := &Vertex{1, 2}  
    v4.test2()  
    println(v4.X, v4.Y)//2 3  
}
  • 不管是指针接收者还是非指针接收者,它在接受一个对象的时候,会自动将这个对象转换为这个方法所需要的类型。

  • 如果一个结构体对象,去调用一个指针接收者的方法,那么这个对象将会自动被取地址(Go会自动解释为这样)转换为指向结构体的指针然后再被调用。

  • 使用值调用者,拷贝的是副本,因此函数内修改不会影响到外面的数据。

    • 因此v1,v3和方法内的v地址不一样
  • 但是按道理,v3是指针,那么拷贝的也是指针,两个指针指向同一个数据。那么修改函数内的v应该会影响到v3,但是实际并没有。这是为什么呢

  • 这是因为go语言中,指针类型的结构体调用值接受者时,go会自动进行解引用操作,以确保方法能够正确地访问结构体的值。这种行为使得我们可以灵活的选择指针还是值来调用方法,而不用担心类型匹配不匹配的问题

值类型地址指针类型地址
v10xc00005deb8v30xc00005df20
v0xc00005ded8v0xc00005dec8
*v30xc00005df08
  • 使用指针接受者,传递给函数的是一个指向当前实例的指针,因此函数内修改会影响到外面的数据
  • 由下表可以看出,v指针解引用后指向的实例其实就是v2,那么值类型其实是自动取地址传递给函数里面的指针参数了
  • 由下表可以看出。v4指针和v是两个不同的指针,v是v4的一份副本。但是两个指针指向实例却是同一个的。
值类型地址指针类型地址
v20xc00005dea8v40xc00005df18
v0xc00005df30v0xc00005df28
*v0xc00005dea8*v0xc00005def8
*v40xc00005def8

小结

  • 指针接收者的方法,可以使用值或者指针来调用。如果是值,go会取地址
  • 值接受者的方法,可以使用值或者指针来调用。如果是指针,go会自动解引用
  • 也不难看出,不管是调用值接受者还是指针接受者,都是拷贝一份被调用者。如果是指针类型,还是浅拷贝,这跟C是一样的
  • 如果用的是函数而不是结构体方法的话,上面两句就会失效了,会直接编译错误(指针类型和值类型不是相同类型)

选择值或指针作为接收者

  • 使用指针接收者的原因如下:
    • 首先,方法能够修改其接收者指向的值
    • 可以避免在每次调用方法时拷贝该值。尤其是值类型为大型结构体时,这样会更加高效,减少不必要的性能开销;如果是小对象或者基本类型,使用值接受者不会有太大的性能影响
    • 一致性考虑: 如果结构体方法集合里,有的需要修改接受者,有的不需要。那么建议统一使用指针接受者,保持一致性。减少使用者调用记忆成本
    • 接口实现:当一个类型实现了某个接口,如果该类型的值和指针都可以满足接口的需要,那么通常推荐使用指针接受者。因为值接受者可以隐式转换为指针接受者,反之不行。这样允许值和指针都能隐式转换为接口类型
  • 通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。

非结构体类型声明方法。

  • 个人理解就是个类型别名,将基础类型封装为结构体类型,当然还可以扩展到数组,切片,映射等。
为基础类型声明方法
  • 一个带 Abs 方法的数值类型 MyFloat
type MyFloat float64
func (f MyFloat) Abs() float64 {
	if f < 0 {
		return float64(-f)
	}
	return float64(f)
}
func main() {
	f := MyFloat(-math.Sqrt2)
	fmt.Println(f.Abs())
}
为数组类型声明方法
  • [3]int变成一个新类型
// 定义一个新的类型 MyArray,它是 [3]int 类型的别名  
type MyArray [3]int  
  
// 为 MyArray 类型声明一个方法 Sum,计算数组元素的和  
func (a MyArray) Sum() int {  
    sum := 0  
    for _, v := range a {  
       sum += v  
    }  
    return sum  
}  
  
func main() {  
    var arr MyArray = [3]int{1, 2, 3}  
    fmt.Println("Sum of array:", arr.Sum())// Sum of array: 6  
}
为切片类型声明方法
  • []int切片变成一个新类型
// 定义一个新的类型 MySlice,它是 []int 类型的别名  
type MySlice []int  
  
// 为 MySlice 类型声明一个方法 Average,计算切片元素的平均值  
func (s MySlice) Average() float64 {  
    if len(s) == 0 {  
       return 0  
    }  
    sum := 0  
    for _, v := range s {  
       sum += v  
    }  
    return float64(sum) / float64(len(s))  
}  
  
func main() {  
    slice := MySlice{1, 2, 3, 4, 5}  
    fmt.Println("Average of slice:", slice.Average())// Average of slice: 3 
}
为映射类型声明方法
  • map[string]int类型变成一个新类型
// 定义一个新的类型 MyMap,它是 map[string]int 类型的别名  
type MyMap map[string]int  
  
// 为 MyMap 类型声明一个方法 Total,计算所有键值对的值之和  
func (m MyMap) Total() int {  
    sum := 0  
    for _, v := range m {  
       sum += v  
    }  
    return sum  
}  
  
func main() {  
    m := MyMap{"apple": 10, "banana": 20, "cherry": 30}  
    fmt.Println("Total of map values:", m.Total())// Total of map values: 60  
}

2.13 接口

什么是接口

  • 这是一种特殊的数据类型,把所有具有共性的方法定义在一起,相当于一个方法集合,一组方法签名,其他类型只要实现了这些方法就是实现了这个接口
package main  
  
import "fmt"  
  
type Shape interface {  
    Area() float64  
    Perimeter() float64  
}  
  
// type rect  
type Rect struct {  
    height float64  
    weight float64}  
  
func (p *Rect) Area() float64 {  
    return p.height * p.weight  
}  
func (p *Rect) Perimeter() float64 {  
    return 2 * (p.height + p.weight)  
}  
func main() {  
    var s Shape = &Rect{height: 10, weight: 8}  
    fmt.Println(s.Area())  
    fmt.Println(s.Perimeter())  
}
  • 接口提供了oop的能力
  • C++,java需要主动声明base类,而go只需要实现某个接口的全部方法(不全实现就会CE)就是实现了该接口。
  • 而golang只需要实现某个接口的全部方法(不全实现就会CE),就是实现了该类型。
  • golang的继承关系是非侵入式的:没有将类型和接口显式绑定,如java的implement,隐式接口从接口的实现中解耦了定义,这样接口的实现可以出现在任何包中,无需提前准备。因此,也就无需在每一个实现上增加新的接口名称,这样同时也鼓励了明确的接口定义。

接口类型的变量可以持有任何实现了该接口的类型的实例

type Abser interface {  
    Abs() float64  
}  
  
type MyFloat float64  
  
func (f MyFloat) Abs() float64 {  
    if f < 0 {  
       return float64(-f)  
    }  
    return float64(f)  
}  
  
type Vertex struct {  
    X, Y float64  
}  
  
func (v *Vertex) Abs() float64 {  
    return math.Sqrt(v.X*v.X + v.Y*v.Y)  
}  
  
func main() {  
    var a Abser  
    f := MyFloat(-math.Sqrt2)  
    v := Vertex{3, 4}  
  
    a = f                // a MyFloat 实现了 Abser    fmt.Println(a.Abs()) //1.4142135623730951  
  
    a = &v               // a *Vertex 实现了 Abser    fmt.Println(a.Abs()) //5  
}
  • v 是一个 Vertex(而不是 *Vertex)。该类型没有实现接口,所以会编译错误 a=v
  • 看出来了吧,这类似于其他语言的继承机制之一:父类对象(静态类型)可以保存任何子类对象(动态类型),并且调用方法实际是调用的子类对象的方法。这实现了继承多态

接口值

  • 接口也是值,可以像其他值一样传递。
  • 接口值可以作为函数的参数或者返回值
  • 在内部,可以将接口值看成一个包含值和具体类型的元组(value,type)。
  • 接口值保存了一个具体实现接口的底层类型的值。 显然这个type是动态类型,value就是保存的具体实现该接口的类型的实例。。
  • 接口值调用方法时会执行其底层类型的同名方法。
type I interface {  
    M()  
}  
  
type T struct {  
    S string  
}  
  
func (t *T) M() {  
    fmt.Println(t.S)  
}  
  
type F float64  
  
func (f F) M() {  
    fmt.Println(f)  
}  
  
func describe(i I) {  
    fmt.Printf("(%v, %T)\n", i, i)  
}  
  
func main() {  
    var i I  
  
    i = &T{"Hello"}  
    describe(i)//(&{Hello}, *main.T)  
    i.M()//Hello  
  
    i = F(math.Pi)  
    describe(i)//(3.141592653589793, main.F)  
    i.M()//3.141592653589793  
}

nil接口值

  • nil切片即底层值没有保存任何东西的接口值,即没有保存任何底层类型
  • nil切片可以调用方法,为nil接口调用方法,将会触发panic,因为底层的元组不能指明调用的是哪个底层类型的方法。在一些语言中,这会触发空指针异常,但是我们也有一些方法来处理它,如本例的M方法
  • 注意,保存了nil具体底层对象的接口,不是nil接口,为该接口调用方法不会触发panic
  • 用上面提到的元组来理解nil接口,那么nil接口是(nil,nil)。而保存了nil具体底层对象的接口就是(nil,type)
package main  
  
import "fmt"  
  
type I interface {  
    M()  
}  
  
type T struct {  
    S string  
}  
  
func (t *T) M() {  
    if t == nil {  
       fmt.Println("<nil>")  
       return  
    }  
    fmt.Println(t.S)  
}  
  
func describe(i I) {  
    fmt.Printf("(%v, %T)\n", i, i)  
}  
  
func main() {  
    var i I  
    i.M() //panic: runtime error: invalid memory address or nil pointer dereference  
  
    var t *T  
    i = t  //底层类型的值为nil
    describe(i) //(<nil>, *main.T)  
    i.M()       //<nil>  
  
    i = &T{"hello"}  
    describe(i) //(&{hello}, *main.T)  
    i.M()       //hello  
}

空接口

  • 包没有包含方法的接口值被称为空接口 interface{}
  • 空接口可保存任何类型的值(每个类型都至少实现了零个方法)(类似java的object)
  • 空接口被用来处理未知类型的值。
  • 空接口被用来处理未知类型的值。
func describe(i interface{}) {  
    fmt.Printf("(%v, %T)\n", i, i)  
}  
  
func main() {  
    var i interface{}  
    describe(i)//(<nil>, <nil>)  
  
    i = 42  
    describe(i)//(42, int)  
  
    i = "hello"  
    describe(i)//(hello, string)  
}

2.14 错误处理

  • go语言习惯使用一个单独的返回值来传递错误信息,这样可以很清晰的知道哪个函数返回了错误
type user struct {  
    name string  
    pass string}  
  
func findUser(users []user, name string) (v *user, err error) {  
    for _, u := range users {  
       if u.name == name {  
          return &u, nil  
       }  
    }  
    return nil, errors.New("not found")  
}  
  
func main() {  
    u, err := findUser([]user{{"wang", "1024"}}, "wang")  
    //要判断error是否存在,否则可能会出现空指针错误  
    if err != nil {  
       fmt.Println(err)  
       return  
    }  
  
    fmt.Println(u.name) // wang  
    if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {  
       fmt.Println(err) // not found  
       return  
    } else {  
       fmt.Println(u.name)  
    }  
}

2.15 字符串

  • 在go中,字符串是一种基本类型。这点与C不同,C没有原生的字符串类型,而是用字符数组表示,并通过字符指针传递字符串
  • go的字符串是一个不可变的UTF8字符串序列。这样不仅可以减少内存的使用,还可以统一编码格式,有助于减少读取文件时的编码和解码工作

rune类型

  • int32的别名,用于表示一个Unicode字符
    • 常用于表示单个Unicode字符
    • 常用于处理字符串中的单个字符,因为每个字符都是UTF8
    • 单引号定义一个rune类型的字面量

字符串字面量

  • go有两种字符串字面量
    • 解释型字符串
      • 就是双引号括起来的字符串,其中的转义字符会被替换掉,不能换行
    • 非解释型字符串
      • 用反引号(md里面表示代码句)括起来的。转义字符不会被解释,并且还支持换行
s1 := "Hello\nWorld!"  
s2 := `Hello\n  
          nick!`fmt.Println(s1)  
fmt.Println(s2)

![[Pasted image 20241107215724.png]]

字符串的长度

  • 函数len(str)可以返回一个字符串的字节数
s := "abc你"  
fmt.Printf("字符串的字节长度是:%d\n", len(s)) // 输出6  
  • 如果要获取字符数量,就要把字符串转换为rune类型切片(数组)
s := "abc你"  
r := []rune(s)  
fmt.Print(len(r))//4

遍历字符串

  • 使用下标索引
for i := 0; i < len(s); i++ {  
    fmt.Printf("%c", s[i])  
}  
for _, v := range s {  
    fmt.Printf("%c", v)  
}
  • 使用range for
for _, v := range s {  
    fmt.Printf("%c", v)  
}
  • 区别是法一如果有非单字节字符,会输出乱码,如果能保证编程中出现的都是ASCLL字符,就可以使用法一

修改字符串

  • string类型是不能修改的,但是我们可以把他拷贝给其他类型来实现
s := "Hello 我是"  
b := []byte(s) // 转换为 []byte,数据被自动复制  
c := []rune(s)    // 转换为 []rune,数据被自动复制

2.17 字符串操作

  • 字符串是一种基本的类型,也是很重要的类型。因此很多编程语言都会封装一些方法来处理字符串。Golang的标准库就存在strings包,提供一些方法
  • 字符串可以通过切片操作获得一个子段,跟slice切片一样
  • 可以通过+连接字符串

查找子串,字符等

  • strings.Contains:检查一个字符串是否包含另一个指定的子字符串。
  • strings.ContainsAny:检查一个字符串是否包含任意一个指定字符集合中的字符
  • strings.count:检查一个字符串包含了几个指定的子字符串

获取子串的索引

  • strings.Index:查找子字符串第一次出现的位置。
  • strings.LastIndex:查找子字符串最后一次出现的位置。
  • strings.IndexByte:查找字符第一次出现的位置。
  • strings.IndexRune:查找指定的 rune 在字符串中第一次出现的位置。
  • strings.IndexAny:查找字符集合中第一个出现的字符的位置。

修剪

  • strings.Trim:去除字符串两端的指定字符。
  • strings.TrimLeft:去除字符串左端的指定字符。
  • strings.TrimRight:去除字符串右端的指定字符。
  • strings.TrimPrefix:如果字符串以指定的前缀开头,则去除该前缀。
  • strings.TrimSuffix:如果字符串以指定的后缀结尾,则去除该后缀。
  • strings.TrimSpace:去除字符串两端的空白字符。

分隔与拼接

  • strings.Split:将字符串按照指定的分隔符分割成子字符串。
  • strings.SplitN:将字符串按照指定的分隔符分割成子字符串,最多分割 n 次。
  • strings.Fields:将字符串按照空白字符分割成子字符串。
  • strings.Join:将字符串切片中的元素用指定的分隔符连接成一个字符串。
  • strings.Repeat:返回一个由字符串重复指定次数组成的字符串。

2.16 字符串格式化

  • go标准库fmt包提供了多种方法实现字符串的格式化
  • fmt.Sprintf:生成格式化的字符串并返回。
  • fmt.Fprintf:将格式化的字符串写入 io.Writer
  • fmt.Printf:将格式化的字符串直接打印到标准输出。

格式化动词

  • %v:默认格式。
  • %T:类型的字符串表示。
  • %t:布尔值,显示为 true 或 false
  • %d:十进制整数。
  • %x:十六进制整数。
  • %b:二进制整数。
  • %c:对应的 Unicode 字符。
  • %s:字符串。
  • %q:双引号括起来的字符串,适合打印 JSON。
  • %p:指针的地址。
  • %f:浮点数。
  • %e:科学记数法的浮点数。
  • %g:根据数值大小选择 %e 或 %f
  • %#v:Go 语法表示的值。
  • %#x:带有前缀 0x 的十六进制整数。
type point struct {
    x, y int
}

func main() {

    s := "hello"

    n := 123

    p := point{1, 2}

    fmt.Println(s, n) // hello 123

    fmt.Println(p)    // {1 2}

    //可以使用%v打印任意类型的变量
    fmt.Printf("s=%v\n", s)  // s=hello

    fmt.Printf("n=%v\n", n)  // n=123

    fmt.Printf("p=%v\n", p)  // p={1 2}

	// +%v 打印更详细的结果, %#v则更详细
    fmt.Printf("p=%+v\n", p) // p={x:1 y:2}
    fmt.Printf("p=%#v\n", p) // p=main.point{x:1, y:2}

    f := 3.141592653

    fmt.Println(f)          // 3.141592653
    fmt.Printf("%.2f\n", f) // 3.14
}

2.17 数字解析(strconv包)

  • 主要用于字符串和数类型的转换

字符串转数字

  • Atoi(s string) (i int, err error):将字符串 s 转换为整数 int 类型。如果转换失败,则返回错误。
  • ParseInt(s string, base int, bitSize int) (i int64, err error):将字符串 s 解析为有符号整数 int64base 参数指定了字符串的基数(如2表示二进制,8表示八进制,10表示十进制,16表示十六进制等),而 bitSize 表示结果应该占用多少位(例如8、16、32或64)。最常用的base参数是0,会函数自行推导
  • ParseUint(s string, base int, bitSize int) (n uint64, err error):与 ParseInt 类似,但是解析无符号整数 uint64

数字转字符串

  • Itoa(i int) string:将整数 i 转换为字符串。
  • FormatInt(i int64, base int) string:以指定的基数 base 将有符号整数 i 转换为字符串。
  • FormatUint(i uint64, base int) string:以指定的基数 base 将无符号整数 i 转换为字符串。
  • FormatFloat(f float64, fmt byte, prec int, bitSize int) string:格式化浮点数 f 为字符串。fmt 可以是 'e', 'E', 'f', 'F', 'g', 或 'G',分别代表科学记数法和普通记数法的不同变体;prec 指定精度;bitSize 指定结果应该是32位还是64位的浮点数。
f, _ := strconv.ParseFloat("1.234", 64)  
fmt.Println(f) // 1.234  
  
n, _ := strconv.ParseInt("111", 10, 64)  
fmt.Println(n) // 111  
  
n, _ = strconv.ParseInt("0x1000", 0, 64)  
fmt.Println(n) // 4096  
  
n2, _ := strconv.Atoi("123")  
fmt.Println(n2) // 123  
  
n2, err := strconv.Atoi("AAA")  
fmt.Println(n2, err) // 0 strconv.Atoi: parsing "AAA": invalid syntax

2.18 JSON处理

  • Go语言提供了一个强大的标准库 encoding/json,用于处理JSON数据。
  • 这个库提供了两个主要的功能:
    • 序列化(将Go的数据结构转换为JSON格式)
    • 反序列化(将JSON格式的数据解析为Go的数据结构)。通常用于从网络接收JSON数据或读取存储在文件中的JSON数据
  • 对于一个结构体,保证每个字段的第一个字母是大写,也就是公开字段,这个结构体就能使用json.Marshal序列化,变成一个json的字符串。然后可以用json.Unmarshal反序列化到一个空变量里。
  • 这样默认序列化出来的字符串,风格是大写字母开头,可以用json tag语法来修改json结果里面的字段名

JSON标签

  • 标签告诉编码器和解码器如何处理字段名。
  • 如果不想某个字段出现在最终的JSON输出中,可以在该字段后面添加 - 标签:
type userInfo struct {  
    Name string  
    Age   int `json:"age"`  
    Hobby []string  
}  
  
func main() {  
  
    a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}  
  
    buf, err := json.Marshal(a)  
    if err != nil {  
       panic(err)  
    }  
    //这是序列号后的二进制序列  
    fmt.Println(buf) // [123 34 78 97...]  
    fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}  
  
    buf, err = json.MarshalIndent(a, "", "\t")  
    if err != nil {  
       panic(err)  
    }  
    fmt.Println(string(buf))  
  
    var b userInfo  
    err = json.Unmarshal(buf, &b)  
    if err != nil {  
       panic(err)  
    }  
    fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}  
}

自定义序列化和反序列化

  • 对某些类型进行特殊的序列化或反序列化处理。可以通过实现 json.Marshalerjson.Unmarshaler 接口来达到目的:
import (  
    "time"  
)  
  
type MyTime struct {  
    time.Time  
}  
  
func (mt MyTime) MarshalJSON() ([]byte, error) {  
    return []byte("\"" + mt.Format("2006-01-02") + "\""), nil  
}  
  
func (mt *MyTime) UnmarshalJSON(data []byte) error {  
    str := string(data)  
    t, err := time.Parse(`"`+"2006-01-02"+`"`, str)  
    if err != nil {  
       return err  
    }  
    mt.Time = t  
    return nil  
}

2.19 时间处理

	//获取当前时间
	now := time.Now()
    fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933

	//构造一个带时区的时间,会有很多方法
    t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
    t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)

    fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC

    fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25

    fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36

	//计算时间差
    diff := t2.Sub(t)

    fmt.Println(diff)                           // 1h5m0s

    fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900

    t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")

    if err != nil {

        panic(err)

    }

    fmt.Println(t3 == t)    // true
	//获取时间戳
    fmt.Println(now.Unix()) // 1648738080

2.20 进程信息

    // go run example/20-env/main.go a b c d
    fmt.Println(os.Args)           // [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_m0000gn/T/go-build3406981276/b001/exe/main a b c d]
    //得到一个长度为5的slice,第一个成员表示二进制自身的名字

	//获取,写入环境变量
    fmt.Println(os.Getenv("PATH")) // /usr/local/go/bin...
    fmt.Println(os.Setenv("AA", "BB"))
    
	//go语言中执行外部命令并处理其输出和错误
    buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()

    if err != nil {

        panic(err)

    }

    fmt.Println(string(buf)) // 127.0.0.1       localhost