104.【GoLang基础】
GO语言
- (一)、GoLang环境安装
-
- 1.安装基本环境包
- 2.配置全局环境
- 3.安装编译器 GoLand
- (二)、第一个GoLang项目
-
- 1.first Model
- (三)、变量的定义和初始化
-
- 1.变量的定义
-
-
- (1).初始基本的数据类型的变量
- (2).GO语言变量命名的独特优点
-
- 2.变量的初始化💥
-
-
- (1).定义和初始化分离
- (2).短变量声明初始化 (语法糖)
-
- 3.理解变量 (内存地址)
- 4.变量的交换 💥
- 5.匿名变量💥
- 6.变量的作用域
- 7.常量
- 8.常量iota💥
- (四)、基本数据类型
-
- 1.布尔类型 (bool)
- 2.数字型 (int float byte)
- 3.字符串 (string)
- 4.数据类型的转换
- (五)、派生数据类型
-
- 1.指针类型 (*)
- 2.数组类型 ([])
- 3.切片类型 (slice)💥
- 4.函数类型 (func)💥
- 5.管道类型 (channel)
-
-
- (1).正常读和写
- (2).发生死锁
- (3).如何解决死锁
-
- 6.接口类型
- 7.结构化类型
- 8.map类型
- (六)、Go语言运算符
-
- 1.算数运算符 (+ - * /)
- 2.关系运算符 (> = <)
- 3.逻辑运算符 (&& || !)
- 4.位元素符 (& | ^ &^ << >>) 💥
- 5.赋值运算符(= += -= /= &= |=)
- 7.键盘的输入与输出 💥
- (七)、流程控制
-
- 1.If语句 (选择结构)
- 2.Switch语句 (选择结构)
-
-
- (1).switch语句
- (2).fallthrough 穿透
- (3).break 终止穿透
-
- 3.For循环 (循环结构)
-
-
- (1).for循环语句
- (2).四乘五方阵
- (3).九九乘法表
- (4).break与continue💥
- (5).遍历string 💥
-
- (八)、函数
-
- 1.什么是函数?
- 2.函数的声明和调用
- 3.函数的参数
-
-
- (1).形式参数和实际参数
- (2).可变参数
- (3).参数的传递
-
- 4.函数变量的作用域
- 5.递归函数 (自己调用自己)
- 6.延迟函数(defer)
- (九)、函数的数据类型 (进阶)
-
- 1.函数类型的变量
- 2.匿名函数推导
- 3.函数式编程
- 4.闭包
(一)、GoLang环境安装
1.安装基本环境包
官网: https://go.dev/dl/
检查是否安装成功!
go version
2.配置全局环境
配置全局环境
配置工作空间
在工作空间自己创建三个文件夹
查看我们的PATH路径配置,发现帮我们自动配置了
用户变量的工作目录也要修改
测试是否成功没?
go env
go version
3.安装编译器 GoLand
官网: https://www.jetbrains.com/go/download/#section=windows
傻瓜式一路安装
(二)、第一个GoLang项目
1.first Model
- 在Go语言里,命名为main的包具有特殊的含义,Go语言的编译程序会试图把这种名字的包编译为二进制可执行文件,所有Go语言编译的可执行程序都必须会有一个名叫 main 的包。一个可执行程序有且仅有一个main包。
- 当编译器发现某个包的名字为main的时候,它一定也会发现名为main()的函数,否则不会创建可执行文件。main()函数是程序的入口,所以,如果没有这个函数,程序就没有办法开始执行。程序编译时,会使用main包的代码所在的目录的目录名作为二进制可执行文件的文件名。
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数fmt.Println("HELLO WORLD!") //打印输出
}
(三)、变量的定义和初始化
1.变量的定义
变量: 就是变化的量
(1).初始基本的数据类型的变量
字符串string
var 变量名 string= "value"
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数var name string = "jsxs" // 定义类型是string 变量是name的字符串name = "lm" // 变量可以被更改,常量一旦定义之后就不能更改了fmt.Println(name) //打印输出
}
整形 (int)
var 变量名 int= value
打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数var name string = "jsxs" // 定义类型是string 变量是name的字符串name = "lm" // 变量可以被更改,常量一旦定义之后就不能更改了var age int = 20fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
}
同时定义多个类型变量
常规定义变量
变量的命名遵循驼峰命名
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数var name string = "jsxs" // 定义类型是string 变量是name的字符串name = "lm" // 变量可以被更改,常量一旦定义之后就不能更改了var age int = 20fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
}
特殊定义变量💥
var(变量名1 变量类型1 = value1变量名2 变量类型2 = value2
)
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数// 如果想要同时定义多个不同类型的变量有两种方式。var (name string //字符串默认为nullage int //整形默认为0)fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
}
(2).GO语言变量命名的独特优点
可以在一行中定义多个变量,而Java一行只能定义一个
var name, age string = "李明", "20"
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数var name, age string = "李明", "20"fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
}
2.变量的初始化💥
(1).定义和初始化分离
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数// 如果想要同时定义多个不同类型的变量有两种方式。var (name string //字符串默认为nullage int //整形默认为0)name = "jsxs" //初始化age = 20 //初始化fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号
}
(2).短变量声明初始化 (语法糖)
Go编译器它可以自动推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。
它可以自动地推导一些类型,但是使用也是有限制的;
- 定义变量,同时要进行初始化
- 不能显式的提供数据类型
- 只能用在函数内部,不能随便到处定义
- 因为简介灵活的特点: 简短变量声明被广泛的用于大部分局部变量的声明和初始化。
- 由于使用了:=,而不是复制的=,因此推导声明写法的左值变量必须是没有定义过的变量;若定义过,将会发生编译错错误。
基本语法
变量名 := 值
根据变量名获取类型
fmt.Printf("%T,%T", name, age)
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数/* 利用语法糖实现 定义变量的同时并进行初始化*/name := "jsxs"age := 20fmt.Println("姓名: ", name, " 年龄: ", age) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号fmt.Printf("%T,%T", name, age) //根据变量名查看数据类型
}
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数/* 利用语法糖实现 定义变量的同时并进行初始化*/var name string = "45"var name1 string = "456"name := "jsxs" // no new variables on left side of :=age := 20fmt.Println("姓名: ", name, " 年龄: ", age, name1) // 打印输出如果两个类型想要链接的话,那么用的不是 "+" 号,而是 "," 号fmt.Printf("%T,%T", name, age) //
}
3.理解变量 (内存地址)
占位符 %T 查看类型,
占位符 %d 查看整形值,
占位符 %p 查看地址 &取地址
package main // 这里的包一定要写成 main.import "fmt" // 导入包func main() { // 主函数num := 10fmt.Printf("num:%d,内存地址: %p", num, &num) //占位符 %T 查看类型, 占位符 %d 查看整形值, 占位符 %p 查看地址 &取地址num1 := 100fmt.Printf("num1:%d,内存地址: %p", num1, &num1) //占位符 %T 查看类型, 占位符 %d 查看整形值, 占位符 %p 查看地址 &取地址
}
4.变量的交换 💥
Go语言底层帮助我们实现了变量交换,我们只需要进行简单操作即可
a, b, c = c, b, a // Go语言底层帮助我么实现了。
package main // 这里的包一定要写成 main.
import "fmt"// 导入包func main() { // 主函数/*变量交换----- 其他语言 C C++ Javaa=100; b=200; temp=0;temp=a; a=b; b=temp;*/var a int = 100var b int = 200fmt.Println("交换前: a: ", a, "b ", b)a, b = b, a // Go语言底层帮助我么实现了。fmt.Println("交换后: a: ", a, "b: ", b)
}
5.匿名变量💥
匿名变量的特点是一个下划线"“,”"本身就是一个特殊的标识符,被称为空白标识符,它可以像其他标识符那样用于变量的声明或复制(任何类型都可以赋值给他),但任何赋值给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用使用这个标识符作为变量对其它变量进行赋值运算。使用匿名变量时,只需要在变量声明的地方使用下划线替换即可。
func test() (int, int) { // 第一个括号负责传参,第二个括号负责返回return 100, 200
}
package main // 这里的包一定要写成 main.
import "fmt"// 导入包func test() (int, int) { // 第一个括号负责传参,第二个括号负责返回return 100, 200
}func main() { // 主函数a, b := test() //因为会返回两个值,我们接受一下fmt.Println(a, b)c, _ := test() //返回值有很多,但我们只想要返回值的某一个值。那么我们就是用匿名变量_ 进行接取fmt.Println(c)_, d := test() //返回值有很多,但我们只想要返回值的某一个值。那么我们就是用匿名变量_ 进行接取fmt.Println(d)
}
在编码过程中,可能会遇到没有名称的变量,类型或方法,虽然这不是必须的,但有时候这些做可以极大地增强代码地灵活性,这些变量称为匿名变量。
匿名变量不占用内存空间,不会分配内存,匿名变量与匿名变量之间也不会因为多次声明而无法使用。
6.变量的作用域
一个变量在程序中都有一定的作用范围,称之为作用域。
了解变量的作用域对我们学习Go语言是比较重要的,因为Go语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明的编译错误。
局部变量
定义在方法之内的变量叫做局部变量。
package main // 这里的包一定要写成 main.
import "fmt"// 导入包func main() { // 主函数var name string = "jsxs" // 局部变量var age int = 20 //局部变量fmt.Println(name, age)
}
func aa() {fmt.Println(name) // undefined: name ----报错
}
局部变量的作用域只能在其规定的范围内使用,否则会报错。
全局变量
定义在方法之外的变量称之为全局变量
package main // 这里的包一定要写成 main.
import "fmt"// 导入包
var name string = "jsxs" // 局部变量
func main() { // 主函数var age int = 20 //局部变量fmt.Println(name, age)
}
func aa() {fmt.Println(name) // undefined: name ----报错
}
全局变量和局部变量相等
当和局部变量和全局变量重名,局部会覆盖全局
package main // 这里的包一定要写成 main.
import "fmt"// 导入包
var name string = "jsxs" // 局部变量
func main() { // 主函数var age int = 20 //局部变量name = "liming"fmt.Println(name, age)aa()
}
func aa() {fmt.Println(name)
}
7.常量
常量是一个简单的值的标识符,在程序运行时,不会被修改的量。
const 常量名 [type]= 值
常量中的数据类型值可以是布尔型、数字型(整数、浮点和复数)和字符串型
我们可以省略说明符 [type],因为编译器可以根据变量的值来判断其类型。
- 显式类型定义: const b string=“abc”
- 隐式类型定义: const b=“abc”
多个不同类型的声明可以简写为:
const a, b, c = 3.14, "sdsd", 20
package main // 这里的包一定要写成 main.
import "fmt"// 导入包
func main() { // 主函数const URL string = "www.jsxs1.cn:8089" //显式定义const URL2 = "www.jsxs1.cn:8091" //隐式定义const a, b, c = 3.14, "sdsd", 20fmt.Println(URL)fmt.Println(URL2)fmt.Println(a, b, c)
}
8.常量iota💥
iota , 特殊常量,可以认为是一个可以被编译器修改的常量。iota是go语言的常量计数器。
iota在const关键字出现时将被充值为0 (const内部的第一行之前),const中每新增一行常量声明将使iota基数一次 (iota可理解为const语句块中的行索引)。
iota 可以被用作枚举值:
假如说被其他值给打断了,那么就会复制上面的值直到新出来一个iota
package main // 这里的包一定要写成 main.
import "fmt"// 导入包
func main() { // 主函数const (a = iota //0b // 1c // 2d // 3e // 4f = "sdsds"g // sdsdsh = 100i //100j = iota // 9k // 10)const (m = iotan)fmt.Println(a, b, c, d, e, f, g, h, i, j, k, m, n)
}
(四)、基本数据类型
Go语言是一种静态类型的编程语言,在Go编程语言中,数据类型用于声明函数和变量。数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要大数据的时候才需呀申请大内存,就可以充分利用内存,编译器进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么。
1.布尔类型 (bool)
%T 是类型, %t 是值
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数var isFlag bool = true //默认为falsefmt.Println(isFlag) // 打印输出类型fmt.Printf("类型是: %T, 值是: %t\\n", isFlag, isFlag) // %T是类型 %t是值 \\n是换行
}
2.数字型 (int float byte)
整形 int 和浮点型 float32.float64.Go语言支持整形和浮点型数字,并且支持复数,其中位的运算采用补码。
Go也有基于架构的类型,列如: unit 无符号、int 有符号(可为负数)
序号 | 类型和描述 |
---|---|
1 | byte 类似于uint8 |
2 | rune 类似于 int32 |
3 | uint 32 或 64 |
4 | int 32或 64 |
5 | uintptr 无符号整形,用于存放一个指针 |
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数// 定义一个整形var age int = 18fmt.Printf(" %T , %d ", age, age) // 类型依旧是 %T; 但整形是%d取值// 定义一个浮点型var money float32 = 20.31fmt.Printf(" %T , %.2f ", money, money) // 类型依旧是 %T; 但浮点型是%f取值->默认保留6位小数,%.nf 保留n位小数
}
3.字符串 (string)
字符串就是遗传固定长度的字符链接起来的字符序列
Go的字符串是由单个字节连接起来的,GO语言的字符串使用UTF-8编码表示和Unicode文本。
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数// 字符串 -> 双引号var str stringstr = "hello jsxs"fmt.Printf(" %T , %s\\n", str, str) // 类型依旧是%T, 但是字符串的值是: %s// 字符 -> 单引号var v1 int32 = 'A'v2 := 'B'fmt.Printf("%T,%c\\n", v1, v1)fmt.Printf("%T,%c\\n", v2, v2)fmt.Println("hello"+"world") //字符串的拼接使用+或者,进行链接,其他的使用,进行链接fmt.Println("hello ", "world") //字符串的拼接使用+号进行链接,字符串和其他类型拼接需要使用,进行链接
}
- Go语言的字符串链接可以通过 + 实现 或者 , 实现
- Go语言的字符串与其他非字符串的链接使用的是: ,
- 双引号指的是: 字符串, 单引号指的是 字符
- 字符用 %c来定义,其类型是 int32
- 转义字符 \\n 换行 \\t 空格
4.数据类型的转换
在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值,由于Go语言不存在隐式类型转换,因此所有的类型转化都必须显式的声明:
转换成的类型type(被转换的变量名)
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数var (a int = 5b float32 = 5.0)// 需求: 将int类型的a转换位 float64类型var c float64 = float64(a)d := int(b)fmt.Printf("%T\\n", a)fmt.Printf("%T\\n", b)fmt.Printf("%T\\n", c)fmt.Printf("%T\\n", d)
}
类型转换只能定义正确的情况下才能转换成功,列如一个取值范围较小的类型转换到一个取值范围较大的类型 (将int16转换到int32).当从一个取值范围较大的类型转换到取值范围较小的类型时(将int32转换到int16时),会发生精度丢失的问题(截断)
(五)、派生数据类型
1.指针类型 (*)
指针变量指向了一个值的内存地址
指针类型的声明格式为var var_name *var-type,使用*类型
表示为某某类型的指针。
- 比如
var ptr *string就表示ptr为指向string类型的指针
。指针的赋值为&变量名称
。 - 比如 var a = “abcde”;var ptr = &a,同样的,指针类型可以根据值自动判断类型。
- 如果只定义了指针变量,而并没有给他赋值,那么该指针就会指向一个空的内存地址,也就是nil,类似其他语言中的null
- 指针的意思: 就是指向一个地址,它的值是一个门牌号(地址)
var var_name *var-type
package main // 这里的包一定要写成 main.
import "fmt"var a = "abcde"
var ptr *string //声明一个string类型的指针
var ptr1 *string = &a //声明一个string类型的指针func main() { // 主函数fmt.Println(ptr) //当前指针ptr为空ptr = &a //让ptr指向a的内存地址fmt.Println(ptr) //现在ptr指向a的内存地址了fmt.Println(ptr1) // 获取地址fmt.Println(*ptr1) //获取值
}
2.数组类型 ([])
数组是一组长度固定的统一数据类型的集合,注意:不同长度的数组属于不同的数据类型
数组的声明方式也很简单var a [n]int,n为数组的长度。后面我们会认识切片slice,其与数组最大的区别就是切片没有固定长度,是对数组的引用,所以在GO中切片比数组要常用许多。
数组的三种定义方式
// 数组的第一中定义var a [3]int = [3]int{1, 2, 3}// 数组的第二种定义var b [3]intb[0] = 1// 数组的第三种定义方式c := [3]int{1, 2, 3}fmt.Println(a, " ", b, " ", c)
for i, v := range a { // 这里的i是 idex,v是 valuefmt.Printf("%d %d\\n", i, v)}
数组的遍历
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数a := [3]int{1, 2, 3} // 定义长度为3的数组,并且初始化值为1,2,3fmt.Println("以下是下标和值: ")//range循环数组的时候可以循环下标和值for i, v := range a { // 这里的i是 idex,v是 valuefmt.Printf("%d %d\\n", i, v)}fmt.Println("以下是值: ")//也可以只循环值,把下标丢弃for _, v := range a {fmt.Printf("%d\\n", v)}
}
3.切片类型 (slice)💥
切片的声明与数组类似,只不过是没有长度,var s []int,因为切片是对数组的引用,所以只声明而未初始化时切片的值为nil。
在go语言中,切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集;切片的内存分布是连续的,所以可以把切片当做一个大小不固定的数组。切片有三个字段的数据结构:指向底层数组的指针、切片访问的元素的个数(即长度)和切片允许增长到的元素个数(即容量)。
1. 获取全部slice[:]2.获取指定区域 左闭右开slice[start:end]3.从指定区域到尾部slice[:end]4.从头部到指定区域silce[start:]5.切片的三种定义:var strList []string //与数组的最大区别就是不要写参数,而数组需要写参数//生命整形切片var numList []int //与数组的最大区别就是不要写参数,而数组需要写参数//声明一个空的切片var numListEmpty = []int{} //与数组的最大区别就是不要写参数且左边不用编写类型,而数组需要写参数
package main // 这里的包一定要写成 main.
import "fmt"/
切片单独使用几乎没有什么用处,需要配合数组使用
*/func main() { // 主函数//声明字符串切片var strList []string //与数组的最大区别就是不要写参数,而数组需要写参数//生命整形切片var numList []int //与数组的最大区别就是不要写参数,而数组需要写参数//声明一个空的切片var numListEmpty = []int{} //与数组的最大区别就是不要写参数且左边不用编写类型,而数组需要写参数// 输出三个切片fmt.Println(strList, " ", numList, " ", numListEmpty)fmt.Println("以上是输出三个切片")// 输出三个切片的大小fmt.Println(len(strList), " ", len(numList), " ", len(numListEmpty))fmt.Println("以上是输出三个切片的大小")//判定输出结果是否位不为空fmt.Println(strList == nil)fmt.Println(numList == nil)fmt.Println(numListEmpty == nil)fmt.Println("以上是判断切片是否为空")fmt.Println("以下是数组的指向")// 数组的第一中定义var a [3]int = [3]int{1, 2, 3}// 数组的第二种定义var b [3]intb[0] = 1// 数组的第三种定义方式c := [3]int{1, 2, 3}fmt.Println(a, " ", b, " ", c)fmt.Println("以下是切片指向数组")// 已经定义的切片指向数组numList = a[:]fmt.Println(numList)// 未定义的切片指向数组strlist1 := a[:]fmt.Println(strlist1)fmt.Println("以下是替换和追加")// 切片在指定索引处进行替换值numList[0] = 5fmt.Println(numList)// 切片在尾部追加numList = append(numList, 6)fmt.Println(numList)
}
4.函数类型 (func)💥
Go语言中的函数也是一种类型。
第一个小括号是入参的小括号,第二个小括号是返回的类型。如果为nil就可以省略
package main// 参数为两个,且参数是返回一个值func functionName1(a int, b int) (int) {return b + a
}// 假如方法是空的话第二个参数可以省略
func functionName2(a int, b int) {println("第二个方法得数是: ", a, b)
}func functionName3(a int, b int) (int, int) {return a, b
}func main() {println("第一个方法得数是: ", functionName1(1, 3))functionName2(1, 2)var (sum1 intsum2 int)sum1, sum2 = functionName3(1, 2) // 假如有两个或者两个以上的返回值我们需要先接受,也可以使用匿名函数进行接受println("第三个方法得数是: ", sum1, " ", sum2)
}
5.管道类型 (channel)
Channel 是 Go 语言中被用来实现并行计算方程之间通信的类型。其功能是允许线程间通过发送和接收来传输指定类型的数据。其初始值是 nil。
var c1 chan 类型
c1 = make(chan 类型, 容量)var c1 chan [value type]
c1 = make([channel type] [value type], [capacity])
- [value type] 定义的是 Channel 中所传输数据的类型。
[channel type] 定义的是 Channel 的类型,其类型有以下三种:
- “chan” 可读可写——“chan int” 则表示可读写 int 数据的 channel
- “chan<-” 仅可写——“chan<- float64” 则表示仅可写64位 float 数据的 channel
- “<-chan” 仅可读——“<-chan int” 则表示仅可读 int 数据的 channel
[capacity] 是一个可选参数,其定义的是 channel 中的缓存区 (buffer)。如果不填则默认该 channel 没有缓冲区
(unbuffered)。对于没有缓冲区的 channel,消息的发送和收取必须能同时完成,否则会造成阻塞并提示死锁错误。对于
channel 的阻塞和非阻塞将在后面详细介绍。
(1).正常读和写
接受消息和发送消息
package mainfunc main() {var c1 chan int //定义一个channel类型的变量var jsxs intc1 = make(chan int, 100) //对这个channel的变量进行初始化// 我们向c1发送一个消息 (写入)c1 <- 20// 我们从c1中进行接受数据 (读取)jsxs = <-c1// 将我们接受的信息进行打印println(jsxs)
}
(2).发生死锁
只读不写或者只写不读会发生死锁或者读和写不同步都会导致死锁
读和写时间不一致,造成死锁反应
package mainimport ("fmt""time"
)
func main() {var c1 chan stringc1 = make(chan string) //管道读和写func() {time.Sleep(time.Second * 2) //睡眠一会c1 <- "result 1" //传入数据}()fmt.Println("received: '", <-c1, "' from c1") //读出数据
}
(3).如何解决死锁
第一种解决方法:
使用 go 语句进行并行计算
package mainimport ("fmt""time"
)func main() {var c1 chan stringc1 = make(chan string)go func() { //使用go语句time.Sleep(time.Second * 2)c1 <- "result 1"}()fmt.Println("received: '", <-c1, "' from c1")
}
通过 go 语句定义发送操作的方程在另一个线程并行运行,这样发送和接收操作就可以同时发生,从而能够解决死锁问题。
第二种解决方法:
package mainimport ("fmt""time"
)func main() {var c1 chan stringc1 = make(chan string, 1) //这里我们设置了一个长度为 1 的 bufferfunc() {time.Sleep(time.Second * 2)c1 <- "result 1"}()fmt.Println("received: '", <-c1, "' from c1")
}
为 channel 添加一个缓冲区(buffer),这样只要 buffer 没有用尽,阻塞就不会发生,死锁也不会发生。
“不要用共享来完成通信,而是用通信来完成共享” 说的就是推荐使用channel来完成协程之间的同步和通信等需求,而不是使用加锁,虽然GO也提供了完善的锁机制。
6.接口类型
GO中的接口并不是强制要求被实现的。如果一个类型实现了接口的所有方法,那么它就自动的实现了这个接口。这里就要在说一下GO中的函数与方法还是有区别的,函数是单纯的实现一些功能的结构,而方法是有类型归属的
,比如下面的say就是函数,而run和walk就是方法
package mainimport "fmt"type runner interface { //定义一个接口runner,里面存在两个方法run()walk()
}type people struct { //定义一个名字叫做people的结构体name stringage int
}func say() { // 定义一个函数fmt.Println("saying")
}func (peo people) run() { // 实现接口的run方法 (左边是值,右边是变量)fmt.Print("running")
}func (peo people) walk() { //实现接口的walk方法 (左边是值,右边是变量)fmt.Println("walking")
}
func eat(ru runner) { // 定义普通方法fmt.Println("eating")
}
func main() {peo := people{name: "张三",age: 20,}eat(peo)peo.run()
}
因为peo有两个方法run和walk,所以它实现了runner接口,也就可以作为参数传到eat方法内。同时方法的调用也类似于java中的调用,是实例.方法。
7.结构化类型
结构化类型类似于class,使用type和struct关键字声明,结构体中也可以嵌套结构体
type Name struct{}
取值就和类一样,进行点即可。
package maintype student struct { // 定义一个结构体name stringage intclass stringaddr addr
}type addr struct {city stringroad stringnumber int
}func main() {stu := student{ //初始化结构体name: "张三",age: 30,class: "一年一班",addr: addr{ //内嵌结构体city: "xx市",road: "黄河路",number: 132,},}println(stu.name, "\\t", stu.age, "\\t", stu.class, "\\t", stu.addr.city, "\\t", stu.addr.road, "\\t", stu.addr.number)
}
8.map类型
GO中的map也与其他语言中的差不多,是对哈希表的引用,所以map也是引用类型
map[keyType]valueType{}
package mainfunc main() {m := map[string]int{} //声明一个空的mapprintln(m)m1 := map[string]int{ //声明一个有初始值的map"a": 1,"b": 2,}//最常用的make方法创建mapm3 := make(map[string]int)m3["a"] = 1println(m3["a"])m1["c"] = 3 //如果没有当前key则添加一个kv对,存在的话就进行修改println(m1["c"])delete(m1, "a") //删除key为a的kv对}
(六)、Go语言运算符
1.算数运算符 (+ - * /)
package main // 这里的包一定要写成 main.
import "fmt"func main() { // 主函数var (a int = 10b int = 3//c int)// + - * / ++ --fmt.Println(a + b)fmt.Println(a - b)fmt.Println(a * b)fmt.Println(a / b)fmt.Println(a % b)a++fmt.Println(a)a--fmt.Println(a)
}
2.关系运算符 (> = <)
package mainfunc main() {var (a int = 11b int = 10)println(a == b)println(a != b)println(a > b)println(a < b)println(a <= b)println(a >= b)
}
3.逻辑运算符 (&& || !)
&&: 一个为假,全部为假
|| : 一个为真,劝慰真
! : 真为假,假为真
package mainfunc main() {var flag1 bool = truevar flag2 bool = falseif flag1 && flag2 { // 测试逻辑且println(flag1 && flag2, "逻辑且")}if flag1 || flag2 { // 测试逻辑或println(flag1 || flag2, "逻辑或")}if !flag1 { // 测试逻辑非println(!flag1, "逻辑非")}
}
4.位元素符 (& | ^ &^ << >>) 💥
位运算:主要用于二进制。(加密文件)
左移2位 00111100 -> 11110000 右边添加n个0,左边非0的保留
二进制-> %b 右移2位 00111100 -> 00001111 从最右边开始数减少n个值,非0的也不保留
package mainimport "fmt"func main() {/*0为假,1为真。 如何进行比较? 上下进行比较。60 0011 110013 0000 1101------------& 0000 1100 一个为假全为假| 0011 1101 一个为真全为真^ 1100 0001 不同为1,相同为0----从这里开始变成单向了,不是双向了---->> 2<< 2*/var (a uint = 60b uint = 13)var c uint = 0c = a & bfmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %bc = a | bfmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %bc = a ^ bfmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %bc = a << 2fmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %b 左移2位 00111100 -> 11110000 右边添加n个0,左边非0的保留a = 60c = a >> 2fmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %b 右移2位 00111100 -> 00001111 从最右边开始数减少n个值,非0的也不保留b = 13c = b << 2fmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %b 左移2位 0000 1101 -> 00110100 右边添加n个0,左边非0的保留b = 13c = b >> 2fmt.Printf("%d(转换成二进制)->%b\\n", c, c) // 二进制-> %b 右移2位 0000 1101 -> 00000011 从最右边开始数减少n个值,非0的也不保留}
5.赋值运算符(= += -= /= &= |=)
package mainfunc main() {var (a int = 21b int = 1)b += aprintln(b)b -= aprintln(b)b /= aprintln(b)b %= aprintln(b)}
7.键盘的输入与输出 💥
输入格式化的数据 不用用逗号分割。
package mainimport "fmt"/*因为在Go语言中,每一个变量都会分布一个内存地址。
*/func main() {var x intvar y float32var z intvar q float32var m intvar n float32// fmt 包下具有打印和输出//fmt.Printf() 打印输出//fmt.Println() 打印输出并换行//fmt.Print() 格式化输出//fmt.Scan() 输入//fmt.Scanln() 输入并换行//fmt.Scanf() 格式化输入fmt.Println("请输入两个数 1、整数 2、浮点数")fmt.Scanln(&x, &y) // 取地址等待输入fmt.Println("x:", x, " ", "y:", y)fmt.Println("请输入两个数 1、整数 2、浮点数")fmt.Scanf("%d %f", &z, &q) // 取地址等待输入,切记不能用逗号封开fmt.Println("z:", z, " ", "q:", q)fmt.Println("请输入两个数 1、整数 2、浮点数")fmt.Scan(&m, &n) // 取地址等待输入fmt.Println("z:", m, " ", "q:", n)}
(七)、流程控制
程序的流程控制结构: 一共有三种: 顺序结构,选择结构,循环结构。
1.If语句 (选择结构)
条件语句需要开发者通过指定一个或多个条件,并通过测试条件来决定是否执行指定语句,并在条件为flase的情况在执行另外的语包。
package mainfunc main() {var (a int = 12)if a > 20 {println("大于20")} else if a < 15 && a > 10 {println("处于10-15之间")} else {println("处于10以下..")}}
2.Switch语句 (选择结构)
(1).switch语句
switch 语句用于基于不同条件执行不同动作,每一个case分支都是唯一的,从上之逐一测试,直到匹配为止。
这里不像其他语言: 这里是固定的值,没有区间。也不用break
package mainimport "fmt"/*
*
这里不像其他语言: 这里是固定的值,没有区间。也不用break
*/
func main() {var (score int = 75)switch score { //如果不加变量,case 90:fmt.Println("成绩为:A")case 80:fmt.Println("成绩为:B")case 70:fmt.Println("成绩为:C")case 60:fmt.Println("成绩为:D")default:fmt.Println("对不起,您挂科了")}switch { //如果不添加参数,默认是bool的truecase false:println("false")case true:println("true")}}
(2).fallthrough 穿透
switch 默认情况下匹配后就不会执行其他case,如果我们需要执行后面的case,可以使用fallthrough穿透case,使用fallthrough会强制执行后面的case语句,fallthrough不会判断下一条case的表达式结果是否为true.
package mainfunc main() {switch { //如果不添加参数,默认是bool的truecase true:println("true")fallthrough //穿透case false:println("false")}}
(3).break 终止穿透
package mainfunc main() {switch { //如果不添加参数,默认是bool的truecase true:println("true")fallthrough //穿透case false:if true {break // 终止穿透}println("false")}}
3.For循环 (循环结构)
(1).for循环语句
for 条件的初始值; 循环条件; 控制变脸自增或者自减
package mainfunc main() {// for 条件的初始值; 循环条件; 控制变脸自增或者自减// for ; 循环条件; 控制变脸自增或者自减// for{} 会一直循环 循环十次sum := 0for i := 0; i < 10; i++ {sum += i}println(sum)// 打印数组var array [6]int = [6]int{1, 2, 3, 4, 5, 6}for i, v := range array {println("坐标是: ", i, " 值是: ", v)}}
(2).四乘五方阵
package main/*打印一个方阵
*/func main() {for i := 0; i < 4; i++ {for j := 0; j < 5; j++ {print("* ")}print("\\n")}
}
(3).九九乘法表
package main/*打印一个9*9
*/func main() {for i := 1; i <= 9; i++ {for j := 1; j <= i; j++ {print(j, "*", i, "=", i*j, "\\t")}print("\\n")}
}
(4).break与continue💥
break: 跳过整个循环。 continue: 跳过本次循环
package main/*打印一个9*9
*/func main() {for i := 0; i < 10; i++ {if i == 5 {print("\\n")break}print(i, "\\t")}for i := 0; i < 10; i++ {if i == 5 {print(i, "-->被跳过了\\t")}print(i, "\\t")}
}
(5).遍历string 💥
Go中得到字符串是一个字节的切片,可以通过将其内容封装在""中来创建字符串,Go中的字符串是Unicode兼容的,并且是UTF-8编码,字符串是一些字节的集合。
默认打印的是ascll码对应的十进制,我们需要进行%c接收
字符串一旦被定义是不能被修改的,因为字符串里面的字节对应的是byte
第一种遍历方式
package mainimport "fmt"/*打印一个9*9
*/func main() {str := "hello,jsxs"println(str)// 获取字符串的长度 lenfor i := 0; i < len(str); i++ {fmt.Printf("%c\\t", str[i]) //如果我们输出的话,会直接走向ascll码的十进制;所以我们要对其进行格式转化。}
}
第二种遍历方式
package mainimport "fmt"/*打印一个9*9
*/func main() {str := "hello,jsxs"println(str)// 获取字符串的长度 lenfor i := 0; i < len(str); i++ {fmt.Printf("%c\\t", str[i]) //如果我们输出的话,会直接走向ascll码的十进制;所以我们要对其进行格式转化。}print("\\n")// 通过range进行遍历,第一个是index,第二个是值。for _, v := range str {fmt.Printf("%c\\t", v)}}
(八)、函数
1.什么是函数?
- 函数是基本的代码块,用于执行一个任务。
- Go语言最少有一个main函数
- 你可以通过函数来划分不同的功能,逻辑上每个函数执行的是指定的任务。
- 函数声明告诉了编译器函数的名称,返回类型和参数。
2.函数的声明和调用
func Name([变量名 变量类型]) [(返回类型)]{函数体}
- 有参无返回值
- 有参有返回值
- 无参无返回值
- 多参多返回值
package mainfunc printInfo4(x int, y int) (int, int) {return x, y
}func printInfo() {println("无参无返回值")
}func printInfo2(msg string) {println(msg)
}func printInfo3(msg string) string {return msg
}func main() {printInfo()printInfo2("有参无返回")println(printInfo3("一个有参一个返回"))var (a intb int)a, b = printInfo4(1, 2)println("多个有参多个返回", a, b)
}
3.函数的参数
(1).形式参数和实际参数
形参和实参一 一对应
package main// 两个数字比大小// 形式参数: 用于接受外部传入过来的参数叫做形式参数
// 实际参数 传给形参的参数叫做实际参数
func max(x, y int) int {if x > y {return x} else {return y}
}func main() {println(max(1, 2)) //实参
}
(2).可变参数
概念: 一个函数的参数类型是确定的,但参数的个数是不确定的,就可以使用可变参数。
func Name(变量名 ...变量类型)(返回类型){
函数体
}
注意事项:
- 如果一个参数是可变擦书,同时还有其他的参数,可变参数要放在列表的最后
- 一个函数的参数列表最多只有一个可变参数
- 可变参数的值是一个数组,可以用for来遍历
package main// 可变参数
func getSum(nums ...int) int { //这里的参数nums 相当于一个数组。sum := 0for i := 0; i < len(nums); i++ {sum += nums[i]}return sum
}func main() {println(getSum(1, 2, 3, 4)) //实参
}
(3).参数的传递
按照数据的存储特点来分:
- 值类型的数据: 操作的是数据本身 int、string、bool、float64、array…
- 引用类型的数据: 操作的是数据的地址 slice、map、chan
值传递
值传递: 形参改变,实参不变。 内存地址不一样,
package mainimport "fmt"func update(array2 [4]int) {fmt.Println(array2, "接受前的数据")array2[0] = 100fmt.Println(array2, "改变后的数据")
}func main() {// 值传递: 形参改变实参不变var array [4]int = [4]int{1, 2, 3, 4}update(array)fmt.Println(array)
}
引用传递
形参改变,实参也改变。内存地址一样
package mainimport "fmt"func main() {// 切片,可以扩容的数组s1 := []int{1, 2, 3, 4}fmt.Printf("%p %d", s1, s1)update2(s1)fmt.Printf("%p %d", s1, s1)
}
func update2(s2 []int) {fmt.Println("传递前的参数", s2)s2[0] = 100fmt.Println("传递后的参数", s2)}
4.函数变量的作用域
局部变量覆盖全局变量就近原则
package mainvar temp int = 200func main() {temp := 100if b := 1; b <= 100 { // 值;条件temp := 50println("内部的temp->", temp) //局部变量遵循就近原则println("if语句的b->", b)}println("外部的temp->", temp)
}
5.递归函数 (自己调用自己)
定义: 一个函数自己调用自己,就叫做递归函数。
注意: 递归函数需要有一个出口,组件向出口靠近,没有出口就会形成死循环
利用递归函数求和
package mainfunc getSum(n int) int {if n == 1 { //当n等于1的时候,那个函数就变成一个值了。不具有回调函数了。return 1}return getSum(n-1) + n
}
func main() {println("值是:", getSum(5))
}
6.延迟函数(defer)
defer函数或方法: 一个函数或方法的执行被延迟了。
- 我们可以在函数中添加多个defer语句,当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数的返回特别是当你在运行一些打开资源的操作时,遇到错误需要提前返回.在返回前,我们需要关闭相应的资源,不然容易造成资源泄露等问题。
- 如果有很多调用defer,那么defer是采用先进后出(栈)模式、
defer 函数名()
package mainfunc f(s string) {println(s)
}
func main() {f("1")println(2)defer f("3") //会被延迟到最后执行println(4)defer f("6")}
只是最后执行,但参数已经进去了
package mainfunc f(a int) {println(a)
}
func main() {a := 10println("a=", a)defer f(a) //参数已经传递进去了,只是在最后执行。a++println("end a=", a)}
(九)、函数的数据类型 (进阶)
1.函数类型的变量
var f3 func(int, int) int = f2
地址是一样的定义一个函数类型的函数变量
package mainimport "fmt"func f() {}
func f1(int, int) {}
func f2(a, b int) int {return a + b
}// func()本身就是一个数据类型,如何定义一个函数类型的函数变量
func main() {fmt.Printf("%T\\n", f) // func()fmt.Printf("%T \\n", 100) // intfmt.Printf("%T\\n", f1) // func(int, int)fmt.Printf("%T\\n", f2) // func(int, int) int// 定义一个函数类型的变量var f3 func(int, int) int = f2 //那么f3就有了f2的功能println(f3(1, 2)) //拥有了函数2的功能。// 地址一样fmt.Printf("%p\\n", f3)fmt.Printf("%p", f2)
}
2.匿名函数推导
- 匿名函数可以作为另一个函数的值
- 匿名函数可以自己调用自己
- 匿名函数可以具有返回值
func(){}()
package mainfunc f() {println("我是f函数")
}// func()本身就是一个数据类型,如何定义一个函数类型的函数变量
func main() {f()f2 := f // 不加括号就是变量,加括号就是调用f2()// 匿名函数 第一种f3 := func() {println("我是f3函数")}f3()// 如果是匿名函数的话,我们在后面写一个() ,那么就可以自己调用自己.可以有参数和返回值 第二种func(a, b int) {println(a, " ", b)println("我是f4函数")}(1, 2)// 匿名函数也可以有返回值 第三种f5 := func(a, b int) int {return a + b}(1, 2)println("我是f5函数", f5)
}
3.函数式编程
高阶函数: 根据go语言的数据类型特点,可以将一个函数作为另外一个函数的参数。
func1(), func2()
将fun1函数作为fun2这个函数的参数
fun2函数: 就叫做豪杰函数,接受一个函数作为参数的函数
fun1函数: 就叫做回调函数,作为另外一个函数的参数
package mainfunc add(a, b int) int {return a + b
}// 高阶函数 -> 调用函数
func oper(a, b int, c func(int, int) int) int {x := c(3, 4) //在这里面进行赋值return a + b + x
}
func oper2(a, b int, c func(int, int) int) int {x := c(3, 4)return a + b + x
}// func()本身就是一个数据类型,如何定义一个函数类型的函数变量
func main() {println(add(1, 2)) // 打印一个普通函数println(oper(1, 2, add)) //高阶函数println(oper2(1, 2, func(a, b int) int { // 高阶函数- 匿名函数传入return a + b})) //高阶函数
}
4.闭包
一个外层函数中,有内层函数,该内层函数中,会操作外层函数的局部变量并且该外层函数的返回值就是这个内层函数。这个内层函数和外层函数的变量,统称为闭包结构。.
局部变量的生命周期就会发生改变,正常的局部变量会随着函数的调用而创建,随着函数的结构而销毁。但是闭包结构中的外层函数的局部变量并不会随着外层函数的结果而销毁,因为内层函数还在继续使用。
package main// 自增
func increment() func() int { // 返回的类型是 函数类型,函数类型再返回整形 i := 0 // 闭包结构// 定义一个匿名函数,给变量自增并返回fun := func() int { // 内层函数,没有执行i++return i}return fun
}
func main() {f := increment() //这里时调用函数,不是赋值函数类型 -> 新建内存println(f)v1 := f() // 开始自增println(v1)v2 := f() //在原有的基础上继续自增println(v2)f2 := increment() //这里时调用函数,不是赋值函数类型 ->新建内存println(f2)v3 := f2()println(v3) // 从1开始println(f()) // 调用f,在原有的内存上继续新增println(f2()) // 调用f2,在原有的内存上继续新增
}