01-go入门

文章目录

  • Go语言学习
  • 1. 简介
    • 安装
      • windows安装
      • linux安装
      • 编译工具安装-goland
  • 2. 入门
    • 2.1 Helloworld
      • 注释
    • 2.2 变量
      • 初始化
      • 打印内存地址
      • 变量交换
      • 匿名变量
      • 作用域
        • 局部变量
        • 全局变量
    • 2.3 常量
      • iota
    • 2.4 数据类型
      • 布尔
      • 整数
      • 浮点类型
      • 复数
      • 字符串
        • 定义字符串
        • 字符串拼接符`+`
        • 定义多行字符串
      • map数据类型
      • channel管道
    • 2.5 运算符
      • 1) 数据类型转换
      • 2) 算术运算符
      • 3) 关系运算符
      • 4) 逻辑运算符
      • 5) 位运算符
      • 6) 赋值运算符
    • 2.6 获取键盘输入
    • 2.7 流程控制
      • 1) 顺序控制
      • 2) 条件控制
        • if
        • if-else 语句
        • if else-if else 语法
      • 3) 循环控制-for
        • 模拟while循环
        • 无限循环
        • for-range 循环
        • 打印九九乘法表
      • 4) 分支控制
        • switch
        • case穿透
      • 5) 跳转控制
        • goto语句
        • break语句
        • continue
    • 2.8 函数
      • 什么是函数?
      • 普通函数声明
      • 匿名函数
      • 闭包
      • defer 延迟调用
      • 异常处理
        • 普通异常
        • 自定义异常
        • panic 宕机
      • 回调函数
      • 函数式编程
    • 2.9 数组
      • 声明
      • 初始化
      • 总结
      • 访问
      • 遍历
      • 获得数组长度
      • 向函数传递数组
      • 使用数组指针传参
    • 2.10 结构体
      • 什么是结构体
      • 定义
      • 初始化
      • 结构体与数组
      • 切片
      • \*号 和 &号
      • 构造函数
      • 匿名成员变量
      • 方法传值
      • 结构体与tag
      • 结构体中方法的定义与使用
    • 2.11 接口
      • 什么是接口?
      • 注意事项
      • 底层实现
  • 3. 高级
    • 3.1 IO流使用
    • 3.2 泛型

Go语言学习

名称链接备注
go语言源码https://github.com/golang/go
官方文档-安装https://golang.google.cn/dl

1. 简介

Go的三个作者分别是:Rob Pike(罗伯.派克),Ken Thompson(肯.汤普森)和Robert Griesemer(罗伯特.格利茨默)

  • Rob Pike:曾是贝尔实验室(BellLabs)的Unix团队,和Plan 9操作系统计划的成员。他与Thompson共事多年,并共创出广泛使用的UTF-8字元编码。
  • Ken Thompson:主要是B语言、(语言的作者、Unix之父。1983年图灵奖(TuringAward)和1998年美国国家技术奖(National Medal of Technology)得主。他与Dennis Ritchie是Unix的原创者。Thompson也发明了后来衍生出C语言的B程序语言。
  • Robert Griesemer:在开发Go之前是Google V8、Chubby和HotSpotJVM的主要贡献者

故事二

lan Lance Taylor 的加入以及第二个编译器(gcc go)的实现 在带来震惊的同时,也伴随着喜悦。
这对 Go 项目来说不仅仅是鼓励,更是一种对可行性的证明。语言的第二次实现对制定语言规范和确定标准库的过程至关重要,同时也有助于保证其高可移植性,这也是 Go 语言承诺的一部分。
自此之后 lan Lance Tavlor 成为了设计和实现 Go 语言及其工具的核心人物。

故事三:http.HandlerFunc、I/0库

Russ Cox在2008年带着他的语言设计天赋和编程技巧加入了刚成立不久的 Go团队。Russ 发现 Go 方法的通用性意味着函数也能拥有自己的方法,这直接促成了 http.HandlerFunc 的实现,让 Go 一下子变得无限可能的特性。
Russ 还提出了更多的泛化性的想法,比如 io.Reader 和 io.Writer 接口,奠定了所有 I/0 库的整体结构。

故事四:cryptographic

安全专家 Adam Langley 帮助 Go 走向 Google 外面的世界。
Adam 为 Go 团队做了许多不为外人知晓的工作,包括创建最初的 http:/lgolang.org 网站以及 build dashboard.
不过他最大的贡献当属创建了 cryptographic 库。
起先,在我们中的部分人看来,这个库无论在规模还是复杂度上都不成气候。但是就是这个库在后期成为了很多重要的网络和安全软件的基础
并且成为了 Go 语言开发历史的关键组成部分。

故事五:Docker、Kubernetes.

一家叫做 Docker 的公司。就是使用 Go进行项目开发,并促进了计算机领域的容器行业,进而出现了像 Kubernetes 这样的项目。现在,我们完全可以说 Go 是容器语言,这是另一个完全出乎意料的结果。

除了大名鼎鼎的Docker,完全用GO实现。业界最为火爆的容器编排管理系统kubernetes完全用GO实现。之后的Docker Swarm,完全用GO实现。

除此之外,还有各种有名的项目,如etcd/consul/fannel,七牛云存储等等 均使用GO实现。有人说,GO语言之所以出名,是赶上了云时代。但为什么不能换种说法?也是GO语言促使了云的发展。

除了云项目外,还有像今日头条、UBER这样的公司,他们也使用GO语言对自己的业务进行了彻底的重构。

小米的云监控open-falcon.org

安装

windows安装

https://golang.google.cn/dl

image-20240917135304962

安装完成后,在设置的安装目录“C:\Program Files\Go”中生成一些目录和文件,如下图1-9所示。

image-20240917135610752

  • api:每个版本的 api 变更差异。
  • bin:go 源码包编译出的编译器(go)、文档工具(godoc)、格式化工具(gofmt)。
  • doc:英文版的 Go 文档。
  • lib:引用的一些库文件。
  • misc 杂项用途的文件,例如 Android 平台的编译、git 的提交钩子等。
  • pkg:Windows 平台编译好的中间文件。
  • src:标准库的源码。
  • test:测试用例。

在安装完成后,可以通过go env 命令检测是否安装成功。在“命令提示符”界面输入“go env”命令,如果显示如下类似结果则说明安装成功。

image-20240917135651453

新建一下环境变量

image-20240917140338153

image-20240917140527601

用户环境变量和系统环境变量一起修改一下

image-20240917141117636

GOROOT 写的就是 安装go的目录位置

image-20240917140407677

linux安装

登录Go语言官网https://golang.google.cn/dl下载Go语言开发包。

安装

cd /usr/local/
wget https://dl.google.com/go/go1.20.1.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.20.3.linux-amd64.tar.gz
bin/go version

在任意目录下使用终端执行 go env 命令,输出如下结果说明Go语言开发包已经安装成功。

go env

编译工具安装-goland

https://www.jetbrains.com/go/

2. 入门

2.1 Helloworld

image-20240917143047974

package mainimport "fmt"//导入一个系统包fmt用来输出的func main() {fmt.Println("Hello World") // 打印
}

打开文件所在位置

go run helloword.go

image-20240917160546153

或者使用goland点击右上角的按钮

image-20240917160647570

如果是新版本不需要设置,以下命令,也可以运行

go env -w GO111MODULE=off

注释

注释主要的功能就是为了增强代码的可读性,不参与程序的一切功能,Go语言的注释主要分成两类。

package main//导入一个系统包fmt用来输出的
import "fmt"func main() {// 单行注释fmt.Println("Hello World")/*** 文档注释*/fmt.Println("文档注释")/** 多行注释*/fmt.Println("多行注释")
}

2.2 变量

Go语言是静态类型语言,就是所有的类型我们都需要明确的去定义。我们这里先不管其他类型,先了解string,我们用它来表示字符串!
在Go语言中,我们声明一个变量一般是使用 var 关键字:

var name type
  • 第一个 var 是声明变量的关键字,是固定的写法,大家记住即可,你要声明一个变量,就需要一个 var。
  • 第二个 name,就是我们变量的名字,你可以按照自己的需求给它定一个名字。用来后续使用!
  • 第三个 type,就是用来代表变量的类型。

样例

package mainimport "fmt"func main() {// 定义一个字符串的变量var name string = "joker"// 对变量进行修改name = "修改后"// 定义一个数字的变量var age int = 18fmt.Println(name, age)
}

如果你学过 Java、C或者其他编程语言,第一次看到这样的操作肯定不舒服。Go语言和许多编程语言不同,它在声明变量时将变量的类型放在变量的名称之后。这样做的好处就是可以避免像C语言中那样含糊不清的声明形式,例如:int*a,b;。其中只有a是指针而b不是。

如果你想要这两个变量都是指针,则需要将它们分开书写。而在 Go 中,则可以和轻松地将它们都声明为指针类型:

 var a,b *int

变量的命名规则遵循骆驼命名法,即首个单词小写,每个新单词的首字母大写,例如:userfiles和 systemInfo。

我们有时候会批量定义变量,如果每次都单独定义比较麻烦,Go语言支持批量定义变量

package mainimport "fmt"func main() {// 变量声明var (address string = "中国"phone   int    = 123456789)fmt.Println(address, phone)
}

当一个变量被声明之后,如果没有显示的给他赋值,系统自动赋予它该类型的零值:

  • 整型和浮点型变量的默认值为 0和 0.0。
  • 字符串变量的默认值为空字符串。
  • 布尔型变量默认为 false。
  • 切片、函数、指针变量的默认为 nil。
package mainimport "fmt"func main() {// 变量声明var (address stringphone   intsex     bool// 定义一个指针p *int)fmt.Println(address, phone, sex, p)
}

运行出来的结果

image-20240917163022828

初始化

var 变量名 类型=(表达式)

比如,我想定义小k的一些信息,我可以这么表示

package mainimport "fmt"func main() {// 变量声明var (address string = "中国"phone   int = 123456789sex     bool = true// 定义一个指针p *int = new(int))fmt.Println(address, phone, sex, p)
}

短变量声明并初始化

这是Go语言的推导声明写法,编译器会自动根据右值类型推断出左值的对应类型。
它可以自动的推导出一些类型,但是使用也是有限制的;

  • 定义变量,同时显式初始化。
  • 不能提供数据类型。
  • 只能用在函数内部。不能随便到处定义(关于函数,我们后面会讲解,听一下就好这里)
package mainimport "fmt"func main() {// := 自动推到address := "http://www.baidu.com"age := 18fmt.Println(address, age)// %T可以输出变量的类型fmt.Printf("%T %T", address, age)
}

输出结果

image-20240917165549203

因为简洁和灵活的特点,简短变量声明被广泛用于大部分的局部变量的声明和初始化。

注意:由于使用了:=,而不是赋值的=,因此推导声明写法的左值变量必须是没有定义过的变量。若定义过,将会发生编译错误。

// 定义变量 name
var name string
//定义变量 name,并赋值为"kuangshen"
name :="kuangshen

打印内存地址

取地址符号 &变量名字

package mainimport "fmt"func main() {var num intnum = 100fmt.Printf("num:%d,内存地址:%p \n", num, &num) // 取地址符 &变量名num = 200fmt.Printf("num:%d,内存地址:%p", num, &num) // 取地址符 &变量名
}

点击运行

image-20240917170141117

运行结果如下

image-20240917170340290

变量交换

package mainimport "fmt"func main() {// 实现变量交换var a, b int = 1, 2fmt.Println(a, b)a, b = b, afmt.Println(a, b)}

匿名变量

匿名变量的特点是一个下画线"_“,”_"本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。例如:

package mainimport "fmt"/*
定义一个test函数,它会返回两个int类型的值,每次调用将会返回100和200 两个数值。
这里我们先不用管 函数的定义,后面会讲解,
我们用这个函数来理解这个匿名变量。
*/
func test() (int, int) {return 100, 200
}
func main() {// 实现变量交换a, _ := test()_, b := test()fmt.Printf("%d %d\n", a, b)
}

在编码过程中,可能会遇到没有名称的变量、类型或方法。虽然这不是必须的,但有时候这样做可以极大地增强代码的灵活性,这些变量被统称为匿名变量。

匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

作用域

了解变量的作用域对我们学习Go语言来说是比较重要的,因为G0语言会在编译时检查每个变量是否使用过,一旦出现未使用的变量,就会报编译错误。如果不能理解变量的作用域,就有可能会带来一些不明所以的编译错误。

局部变量
package mainimport "fmt"func main() {//声明局部变量a和b并赋值var a int = 3var b int = 4//声明局部变量c 并计算 a和b 的和c := a + bfmt.Printf("a=%d,b=%d,c=%d\n", a, b, c) //a=3,b=4,c=7
}
全局变量

在函数体外声明的变量称之为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,当然,不包含这个全局变量的源文件需要使用“import”关键字引入全局变量所在的源文件之后才能使用这个全局变量。

全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写。

package mainimport "fmt"// 全局变量
var name intfunc aaa() {fmt.Println(name)
}func main() {//声明局部变量a和b并赋值var a int = 3var b int = 4//声明局部变量c 并计算 a和b 的和name = a + bfmt.Printf("a=%d,b=%d,c=%d\n", a, b, name) //a=3,b=4,c=7aaa()// 局部变量和全局变量 可以一样。但是会覆盖全局变量var name int = 10fmt.Printf("a=%d,b=%d,c=%d\n", a, b, name) //a=3,b=4,c=7
}

2.3 常量

常量是一个简单值的标识符,在程序运行时,不会被修改的量,

常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型。

const identifier [type]= value

你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型。

  • 显式类型定义: const b string = “abc”
  • 隐式类型定义: const b = "abc’

多个相同类型的声明可以简写为:

const c_namel,c_name2 =valuel,value2

例如

package mainimport "fmt"const age int = 1
const name = "sss"func main() {fmt.Printf("%T %T", age, name)
}

image-20240917172606646

iota

特殊常量,可以认为是一个可以被编译器修改的常量。iota是go语言的常量计数器

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

iota 可以被用作枚举值:

package mainimport "fmt"const (a = iotab = iotac = iota
)func main() {fmt.Printf("%d %d %d\n", a, b, c)
}

image-20240917172811998

第二种情况?

package mainimport "fmt"const (a = iotabcde = "dasda"fg = iotah
)func main() {fmt.Println(a, b, c, d, e, f, g, h)
}

image-20240917172952447

2.4 数据类型

类型描述
布尔型布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。
数字类型整型 int 和浮点型 float,Go 语言支持整型和浮点型数字,并且原生支持复数,其中位的运算采用补码。
字符串类型字符串就是一串固定长度的字符连接起来的字符序列。Go的字符串是由单个字节连接起来的。Go语言的字符串的字节使用UTF-8编码标识Unicode文本。
派生类型包括:(a) 指针类型(Pointer) (b) 数组类型 © 结构体类型(struct) (d) 联合体类型 (union) (e) 函数类型 (f) 切片类型 (g) 接口类型(interface) (h) Map 类型 (i) Channel 类型

image-20240917182047622

布尔

一个布尔类型的值只有两种:truefalseiffor 语句的条件部分都是布尔类型的值,并且==<等比较操作也会产生布尔型的值。

一元操作符!对应逻辑非操作,因此!true的值为 false,更复杂一些的写法是(!true==false) ==true,实际开发中我们应尽量采用比较简洁的布尔表达式,就像用 x 来表示x==true

package mainimport "fmt"func main() {var num = 10if num == 5 {fmt.Println("num == 5")} else {fmt.Println(num)}if num == 10 {fmt.Println("num == 10")} else {fmt.Println(num)}if num != 5 {fmt.Println("num != 5")} else {fmt.Println(num)}if num != 10 {fmt.Println("num != 10")} else {fmt.Println(num)}
}
// 结果如下
10
num == 10
num != 5
10

布尔值可以和 &&(AND)||(OR)操作符结合,并且有短路行为,如果运算符左边的值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值,因此下面的表达式总是安全的:

短路行为:当有多个表达式(值)时,左边的表达式值可以确定结果时,就不再继续运算右边的表达式的值。

操作符短路描述
&&当左边为True,还需要运算右边,结果由右边决定;当左边为Flase,不需要右边进行运算,结果为Flase
||当左边为True,不需要运算右边,结果为True;当左边为Flase,需要计算右边,结果由右边决定
package mainimport "fmt"func main() {var s1 string = "nihao"// if s1 == "nihao" && s1[0] == 'n' {   //s1 符合要求// if s1 == "nihao" && s1[0] == 'x' {   //s1 不符合要求// if s1 == "nihao" && s1[0] == "n" {   // invalid operation: s1[0] == "n" (mismatched types byte and untyped string)// if s1 != "" && s1[0] == 'x' {     // s1 不符合要求// if s1 != "" && s1[0] == 'n' {        // s1 符合要求// if s1 == "nihao" || s1[0] == 'n' {     //s1 符合要求// if s1 == "nihao" || s1[0] == 'x' {       // s1 符合要求// if s1 != "" || s1[0] == 'x' {        // s1 符合要求if s1 != "" || s1[0] == 'n' {           // s1 符合要求fmt.Println("s1 符合要求")} else {fmt.Println("s1 不符合要求")}
}

其中 s[0] 操作如果应用于空字符串将会导致 panic 异常(终端程序的异常)。
因为&&的优先级比||高(&& 对应逻辑乘法,|| 对应逻辑加法,乘法比加法优先级要高),所以下面的布尔表达式可以不加小括号:

if 'a' <= c && c <= 'z' ||'A' <= c && c <= 'Z' ||'0' <= c && c <= '9' {// ...ASCII字母或数字...
}

布尔值并不会隐式转换为数字值 0 或 1,反之亦然,必须使用 if 语句显式的进行转换:

image-20240917181549052

如果需要经常做类似的转换,可以将转换的代码封装成一个函数,如下所示:

// 如果b为真,btoi返回1;如果为假,btoi返回0
func btoi(b bool) int {if b {return 1}return 0
}

数字到布尔型的逆转换非常简单,不过为了保持对称,我们也可以封装一个函数:

// itob报告是否为非零。
func itob(i int) bool { return i != 0 }

Go语言中不允许将整型强制转换为布尔型,代码如下:

var n bool
fmt.Println(int(n) * 2)

编译错误,输出如下:

image-20240917181637168

Go语言的数值类型分为以下几种:整数、浮点数、复数,其中每一种都包含了不同大小的数值类型,例如有符号整数包含 int8、int16、int32、int64 等,每种数值类型都决定了对应的大小范围和是否支持正负符号。

整数

Go语言同时提供了有符号和无符号的整数类型,其中包括 int8、int16、int32 和 int64 四种大小截然不同的有符号整数类型,分别对应 8、16、32、64 bit(二进制位)大小的有符号整数,与此对应的是 uint8、uint16、uint32 和 uint64 四种无符号整数类型。

序号类型和描述
uint8无符号8 位整型 (0 到 255)
uint16无符号 16 位整型 (0 到 65535)
uint32无符号 32 位整型 (0 到 4294967295)
uint64无符号64 位整型(0到18446744073709551615)
int8有符号 8 位整型(-128 到 127)
int16有符号 16 位整型 (-32768 到32767)
int32有符号 32 位整型(-2147483648 到21474836478)
int64有符号64 位整型(-9223372036854775808到9223372036854775807)

还有其他类型

序号类型和描述
1byte 类似 uint8
2rune 类似 int32
3uint 32 或 64 位
4int 与 uint 一样大小
5uintptr 无符号整型,用于存放一个指针

浮点类型

Go语言提供了两种精度的浮点数 float32float64,它们的算术规范由 IEEE754 浮点数国际标准定义,该浮点数规范被所有现代的 CPU 支持。

这些浮点数类型的取值范围可以从很微小到很巨大。浮点数取值范围的极限值可以在 math 包中找到:

  • 常量 math.MaxFloat32 表示 float32 能取到的最大数值,大约是 3.4e38
  • 常量 math.MaxFloat64 表示 float64 能取到的最大数值,大约是 1.8e308
  • float32float64 能表示的最小值分别为 1.4e-454.9e-324

一个 float32 类型的浮点数可以提供大约 6 个十进制数的精度,而 float64 则可以提供约 15 个十进制数的精度,通常应该优先使用 float64 类型,因为 float32 类型的累计计算误差很容易扩散,并且 float32 能精确表示的正整数并不是很大。如下:

package mainimport "fmt"func main() {var f float32 = 16777216 // 1 << 24var f1 float64 = 16777216fmt.Println(f)                // 1.6777216e+07fmt.Println(f1)               // 1.6777216e+07fmt.Println(f +1)             // 1.6777216e+07fmt.Println(f == f+1)         // truefmt.Println(f1 + 1)           // 1.6777217e+07fmt.Println(f1 == f1 + 1)     // false
}

浮点数在声明的时候可以只写整数部分或者小数部分,像下面这样:

const e = .71828 
const f = 1.     
fmt.Println(e)   // 0.71828
fmt.Println(f)   // 1

很小或很大的数最好用科学计数法书写,通过 e 或 E 来指定指数部分:

const Avogadro = 6.02214129e23  // 阿伏伽德罗常数
const Planck   = 6.62606957e-34 // 普朗克常数

用 Printf 函数打印浮点数时可以使用%f来控制保留几位小数

package mainimport ("fmt""math"
)func main() {fmt.Printf("%f\n", math.Pi)     // 3.141593fmt.Printf("%.2f\n", math.Pi)   // 3.14
}

复数

复数是由两个浮点数表示的,其中一个表示实部(real),一个表示虚部(imag)。

Go语言中复数的类型有两种,分别是 complex128(64 位实数和虚数)和 complex64(32 位实数和虚数),其中 complex128 为复数的默认类型。

复数的值由三部分组成 RE + IMi,其中 RE 是实数部分,IM 是虚数部分,RE 和 IM 均为 float 类型,而最后的 i 是虚数单位。

声明复数的语法格式如下所示:

var name complex128 = complex(x, y)

其中 name 为复数的变量名,complex128 为复数的类型,"="后面的 complex 为Go语言的内置函数用于为复数赋值,x、y 分别表示构成该复数的两个 float64 类型的数值,x 为实部,y 为虚部。
上面的声明语句也可以简写为下面的形式:

name := complex(x, y)

对于一个复数z := complex(x, y),可以通过Go语言的内置函数real(z) 来获得该复数的实部,也就是 x;通过imag(z) 获得该复数的虚部,也就是 y。
使用内置的 complex 函数构建复数,并使用 real 和 imag 函数返回复数的实部和虚部:

var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y)                 // "(-5+10i)"
fmt.Println(real(x*y))           // "-5"
fmt.Println(imag(x*y))           // "10"

复数也可以用==和!=进行相等比较,只有两个复数的实部和虚部都相等的时候它们才是相等的。

Go语言内置的 math/cmplx 包中提供了很多操作复数的公共方法,实际操作中建议大家使用复数默认的 complex128 类型,因为这些内置的包中都使用 complex128 类型作为参数。

字符串

一个字符串是一个不可改变的字节序列,字符串可以包含任意的数据,但是通常是用来包含可读的文本,字符串是 UTF-8 字符的一个序列(当字符为 ASCII 码表上的字符时则占用 1 个字节,其它字符根据需要占用 2-4 个字节)。

定义字符串

可以使用双引号""来定义字符串,字符串中可以使用转义字符来实现换行、缩进等效果,常用的转义字符包括:

  • \n:换行符
  • \r:回车符
  • \t:tab 键
  • \u 或 \U:Unicode 字符
  • \:反斜杠自身
package mainimport "fmt"func main() {var s string = "hello Jack!"var s1 string = "hello \nJack!"var s2 string = "hello \rJack!"var s3 string = "hello \tJack!"var s4 string = "hello \\Jack!"fmt.Println(s)fmt.Println(s1)fmt.Println(s2)fmt.Println(s3)fmt.Println(s4)
}// 结果如下:
hello Jack!
hello 
Jack!
Jack! 
hello   Jack!
hello \Jack!

一般的比较运算符(==、!=、<、<=、>=、>)是通过在内存中按字节比较来实现字符串比较的,因此比较的结果是字符串自然编码的顺序。字符串所占的字节长度可以通过函数 len() 来获取,例如 len(str)。

var s string = "hello Jack!"
var s1 string = "hello \nJack!"
fmt.Println(len(s))   // 11
fmt.Println(len(s1))  // 12

字符串的内容(纯字节)可以通过标准索引法来获取,在方括号[ ]内写入索引,索引从 0 开始计数:

  • 字符串 str 的第 1 个字节:str[0]
  • 第 i 个字节:str[i - 1]
  • 最后 1 个字节:str[len(str)-1]

需要注意的是,这种转换方案只对纯 ASCII 码的字符串有效。
注意:获取字符串中某个字节的地址属于非法行为,例如 &str[i]。

var s string = "hello Jack!"
fmt.Println(s[3])   //108
fmt.Println(s[4])   //111
字符串拼接符+

两个字符串 s1 和 s2 可以通过 s := s1 + s2 拼接在一起。将 s2 追加到 s1 尾部并生成一个新的字符串 s。

可以通过下面的方式来对代码中多行的字符串进行拼接:

str := "Beginning of the string " +
"second part of the string"
fmt.Println(str01)
// 因为编译器会在行尾自动补全分号,所以拼接字符串用的加号“+”必须放在第一行末尾

也可以使用+=来对字符串进行拼接:

str02 := "hel" + "lo"
fmt.Println(str02)
str02 += " world"
fmt.Println(str02)
定义多行字符串

在Go语言中,使用双引号书写字符串的方式是字符串常见表达方式之一,被称为字符串字面量(string literal),这种双引号字面量不能跨行,如果想要在源码中嵌入一个多行字符串时,就必须使用`反引号,代码如下:

str_more := `first line
second line`
fmt.Println(str_more)first line
second line

map数据类型

map类型是一个K/V键值对存储的数据类型,key通过hash算法生成hash值,hash值的低位取模槽位(可以理解为容量,go的底层存储是hash表,表中有多个表节点,即槽位,一个槽位最多存放8对K/V组合,如果存储满了通过overflow指向下一个槽位),获取value存储的槽位的节点,通过高位查询节点中的数据。

map的初始化

package mainimport "fmt"func main() {var m1 map[string]int                  // 声明map, 指定key的数据类型是string,value的数据类型是intm2 := make(map[string]int)             // 初始化,容量是0m3 := make(map[string]int, 200)        // 初始化,容量是200,建议初始化时指定map的容量,减少map扩容m4 := map[string]int{"语文": 2, "数学": 3} // 初始化时赋值fmt.Println(m1, m2, m3, m4)
}

map的添加和删除

m := map[string]int{"语文": 2, "数学": 3} // 初始化时赋值m["英语"] = 10    // 向map中添加数据m["英语"] = 20    // 向map中添加数据,如果key之前有值,会覆盖之前的value数据delete(m, "英语") // 从map中删除key对应的键值对数据len(m)            // 获取map的长度// map不支持使用cap函数

map的查询

value := m["语文"]
fmt.Println(value)

遍历

m := map[string]int{"语文": 2, "数学": 3}
for key, value := range m {fmt.Printf("key = %s, value = %d\n", key, value)
}

channel管道

管道是一个环形先进先出的队列,send(插入)和recv(取走)从同一个位置沿同一个方向顺序执行。

sendx:表示最后一次插入元素的位置。

recvx:表示最后一次取走元素的位置。

管道的初始化

// ch 定义的管道变量
// chan 管道类型关键字
// int 管道里面存放的数据类型
var ch chan int

管道的初始化

// 管道的环形队列里面可容纳8个int类型的长度
ch = make(chan int, 8)

管道的声明时初始化

var ch = make(chan int, 8)

向管道中插入数据 // 管道写满后再写入会阻塞报错,没有自动扩容的这种机制

// 向管道中插入元素,使用 <- 符号
// 语法: 变量 <- 要插入的数据
ch <- 10
ch <- 20
ch <- 50

从管道中取出数据 // 管道写满后,不可以再写入,有数据被取出后,可以再向管道中写

// 向管道中取出元素
// 语法: 赋值变量 = <-管道变量
var v = <-ch
fmt.Println(v)
v = <-ch
fmt.Println(v)
v = <-ch
fmt.Println(v)

遍历

// 遍历管道,遍历管道中剩余的元素
close(ch) // range方式遍历前必须先关闭管道,禁止再写入元素
for value := range ch {fmt.Println(value)
}

2.5 运算符

1) 数据类型转换

在必要以及可行的情况下,一个类型的值可以被转换成另一种类型的值。由于Go语言不存在隐式类型转换,因此所有的类型转换都必须显式的声明:

valueofTypeB =typeB(valueofTypeA)

例子

package mainimport "fmt"func main() {var a float32 = 10.00b := int(a)fmt.Println(a)fmt.Println(b)fmt.Println("Hello, world!")
}

类型转换只能在定义正确的情况下转换成功,例如从一个取值范围较小的类型转换到一个取值范围较大的类型(将int16 转换为 int32)。当从一个取值范围较大的类型转换到取值范围较小的类型时(将 int32 转换为 int16 或将 foat32 转换为int),会发生精度丢失(截断)的情况。

整形是不能转换成bool类型。字符串类型不能强转成int类型

2) 算术运算符

下表列出了所有Go语言的算术运算符。假定 A值为 10,B值为 20。

运算符描述实例
+相加A+B 输出结果 30
-相减A-B输出结果 -10
*相乘A*B输出结果 200
/相除B/A输出结果2
%求余B%A输出结果 0
++自增A++ 输出结果 11
自减A–输出结果 9

例子

package mainimport "fmt"func main() {var a int = 20var b int = 10fmt.Println(a + b)fmt.Println(a - b)fmt.Println(a * b)fmt.Println(a / b)fmt.Println(a % b)a++fmt.Println(a)a=20a--fmt.Println(a)}

3) 关系运算符

运算符描述实例
==检查两个值是否相等,如果相等返回 True 否则返回 False。(A == B)为 False
!=检查两个值是否不相等,如果不相等返回 True 否则返回 False.(A!= B)为 True
>检查左边值是否大于右边值,如果是返回 True 否则返回 False。(A>B)为 False
<检查左边值是否小于右边值,如果是返回True 否则返回 False。(A<B) 为 True
>=检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。(A >= B)为 False
<=检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。(A<= B)为 True

例子

package mainimport "fmt"func main() {var A int = 20var B int = 10fmt.Println("A==B:", A == B)fmt.Println("A!=B:", A != B)fmt.Println("A>B:", A > B)fmt.Println("A<B:", A < B)fmt.Println("A>=B:", A >= B)fmt.Println("A<=B:", A <= B)}

img

4) 逻辑运算符

运算符描述实例(A 为 true,B 为 false。)
&&逻辑与运算符。 如果两边的操作数都是 true,则条件 true,否则为 false。(A && B) 为 false
!逻辑非运算符。 如果条件为 true,则逻辑 NOT 条件 false,否则为 true。!(A && B)

例子

    C := trueD := falsefmt.Println("C&&D:",C&&D)fmt.Println("C||D:",C||D)fmt.Println("!(C&&D):",!(C&&D))

5) 位运算符

运算符描述实例(A为60,B为13)
&按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。(A & B) 结果为 12, 二进制为 0000 1100
^按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。(A ^ B) 结果为 49, 二进制为 0011 0001
<<左移运算符"<<“是双目运算符。左移n位就是乘以2的n次方。 其功能把”<<“左边的运算数的各二进位全部左移若干位,由”<<"右边的数指定移动的位数,高位丢弃,低位补0A << 2 结果为 240 ,二进制为 1111 0000
>>右移运算符">>“是双目运算符。右移n位就是除以2的n次方。 其功能是把”>>“左边的运算数的各二进位全部右移若干位,”>>"右边的数指定移动的位数,低位丢弃,高位补符号位。A >> 2 结果为 15 ,二进制为 0000 1111

例子

package mainimport "fmt"func main() {var A, B intA, B = 60, 13fmt.Println("A&B:", A&B)fmt.Println("A|B:", A|B)fmt.Println("A^B:", A^B)fmt.Println("A<<2:", A<<2)fmt.Println("A>>2:", A>>2)}

结果如下

image-20240919204006686

6) 赋值运算符

运算符描述实例
=简单的赋值运算符,将一个表达式的值赋给一个左值C = A + B 将 A + B 表达式结果赋值给 C
+=相加后再赋值C += A 等价于 C = C + A
-=相减后再赋值C -= A 等价于 C = C - A
*=相乘后再赋值C *= A 等价于 C = C * A
/=相除后再赋值C /= A 等价于 C = C / A
%=求余后再赋值C %= A 等价于 C = C % A
<<=左移后赋值C <<= 2 等价于 C = C << 2
>>=右移后赋值C >>= 2 等价于 C = C >> 2
&=按位与后赋值C &= 2 等价于 C = C & 2
^=按位异或后赋值C ^= 2 等价于 C = C ^ 2
|=按位或后赋值

例子

package mainimport "fmt"func main() {var A int = 20var B int = 10var E intE = A + Bfmt.Println("E:", E)E += Afmt.Println("E += A:", E)E -= Afmt.Println("E -= A:", E)E *= Afmt.Println("E *= A:", E)E /= Afmt.Println("E /= A:", E)E %= Afmt.Println("E %= A:", E)E <<= 2fmt.Println("E <<= 2:", E)E >>= 2fmt.Println("E >>= 2:", E)E &= 2fmt.Println("E &= 2:", E)E ^= 2fmt.Println("E ^= 2:", E)E |= 2fmt.Println("E |= 2:", E)}

2.6 获取键盘输入

第一种方式

package mainimport "fmt"func main() {var x intvar y float64fmt.Println("请输入两个数,1.整数,2.浮点数")fmt.Scan(&x, &y)fmt.Printf("整数是%d,浮点数是%f", x, y)
}

第二种方式

package mainimport ("bufio""fmt""os""strconv""strings"
)func main() {reader := bufio.NewReader(os.Stdin)fmt.Print("请输入数字,以空格分隔: ")input, err := reader.ReadString('\n')if err != nil {fmt.Fprintln(os.Stderr, "读取输入时发生错误:", err)return}// 移除末尾的换行符input = strings.TrimSuffix(input, "\n")// 分割输入的字符串,以空格为分隔符numbers := strings.Split(input, " ")// 将字符串数组转换为整数数组var intNumbers []intfor _, numStr := range numbers {num, err := strconv.Atoi(numStr)if err != nil {fmt.Fprintln(os.Stderr, "转换数字时发生错误:", err)return}intNumbers = append(intNumbers, num)}// 倒序输出数字for i := len(intNumbers) - 1; i >= 0; i-- {fmt.Println(intNumbers[i])}
}

2.7 流程控制

1) 顺序控制

顺序控制是最基本的流程控制,程序按照从上到下的顺序逐行执行。在Go语言中,如果中间没有任何判断和跳转语句,程序就会按照这种默认的顺序执行。

package mainimport "fmt"func main() {var num1 int = 10var num2 int = num1 + 20fmt.Println(num2) // 输出:30
}

在这个例子中,程序首先定义了变量num1并赋值为10,然后定义num2并赋值为num1 + 20,最后打印出num2的值。这个过程完全按照代码的顺序执行,没有任何跳转或条件判断。

2) 条件控制

if

条件控制语句用于根据条件表达式的真假来决定是否执行某段代码。Go语言提供了ifif-elseif-else if-else等结构来实现条件控制。

语法格式

if 条件表达式 {// 条件为真时执行的代码
}

示例代码

package mainimport "fmt"func main() {a := 10if a < 20 {fmt.Println("a 小于 20")}fmt.Println("a 的值为:", a)
}
// 输出:a 小于 20
//       a 的值为: 10
if-else 语句

if-else语句用于在条件表达式为真时执行一段代码,否则执行另一段代码。

语法结构

if 条件表达式 {// 条件为真时执行的代码
} else {// 条件为假时执行的代码
}

实例

package mainimport "fmt"func main() {a := 100if a < 20 {fmt.Println("a 小于 20")} else {fmt.Println("a 不小于 20")}fmt.Println("a 的值为:", a)
}
// 输出:a 不小于 20
//       a 的值为: 100
if else-if else 语法

if-else if-else语句用于处理多个条件分支,当某个条件满足时,执行相应的代码块。

语法

if 条件表达式1 {// 条件表达式1为真时执行的代码
} else if 条件表达式2 {// 条件表达式1为假且条件表达式2为真时执行的代码
} else {// 所有条件都不满足时执行的代码
}

示例代码

package mainimport "fmt"func main() {x := 5if x > 10 {fmt.Println("x 大于 10")} else if x < 5 {fmt.Println("x 小于 5")} else {fmt.Println("x 等于 5")}
}
// 输出:x 等于 5

实战

package mainimport "fmt"// 命令行程序 go build xxx.go 生成可执行程序。
func main() {// 练习:if的练习// 判断用户密码输入是否正确// 输入框 : 接收用户的输入 (新知识)// 第一次判断// 输入框 :请再次输入密码  接收用户的输入 (新知识)// 第二次判断// 如果两次都是对的,那么则登录成功//var num1 intvar num2 int// 提示用户输入fmt.Printf("请输入密码 : \n")//fmt.Scan() 输入。传入的是指针对象,即是内存地址// 接收用户的输入 (阻塞式等待... 直到用户输入之后才继续运行)最简单的人机交互// Scan()  &num1地址,fmt.Scan(&num1) // 等待你的输入,卡住... 如果你输入完毕,回车。输入内容就会赋值给num// 123456 模拟数据,未来是在数据库中查询出来的。根据用户查的if num1 == 123456 {fmt.Println("请再次输入密码: ")fmt.Scan(&num2)if num2 == 123456 {fmt.Println("登录成功")} else {fmt.Println("登录失败")}} else {fmt.Println("登录失败")}}

3) 循环控制-for

Go语言中的for循环是唯一的循环结构,但它足够灵活,可以模拟传统的whiledo-while循环。

for 初始化语句; 条件表达式; 迭代语句 {// 循环体
}

示例代码

package mainimport "fmt"func main() {for i := 0; i < 5; i++ {fmt.Println(i)}
}
// 输出:0
//       1
//       2
//       3
//       4
模拟while循环
package mainimport "fmt"func main() {x := 5for x > 0 {fmt.Println(x)x--}
}
// 输出:5
//       4
//       3
//       2
//       1
无限循环
package mainimport ("fmt""time"
)func main() {for {fmt.Println("This will run indefinitely!")//每隔一秒执行一次time.Sleep(1 * time.Second)// 通常这里会加入某种条件来跳出循环// 例如通过break语句}
}
for-range 循环

for-range循环是Go语言中处理集合(如数组、切片、字符串、映射)的便捷方式。它会自动迭代集合中的每个元素,并返回元素的索引和值(对于映射,则返回键和值)。

for 索引,:= range 集合 {// 循环体
}

实例代码

package mainimport "fmt"func main() {nums := []int{1, 2, 3, 4, 5}for idx, num := range nums {fmt.Printf("Index: %d, Value: %d\n", idx, num)}
}
// 输出:Index: 0, Value: 1
//       Index: 1, Value: 2
//       Index: 2, Value: 3
//       Index: 3, Value: 4
//       Index: 4, Value: 5
打印九九乘法表
package mainimport "fmt"func main() {for i := 1; i < 10; i++ {for j := 1; j <= i; j++ {fmt.Printf("%d * %d = %d\t", j, i, i*j)}fmt.Println()}
}

4) 分支控制

分支控制语句用于根据不同的条件执行不同的代码块。Go语言提供了switch语句来实现分支控制,并允许在switch语句中进行类型匹配(type switch)。

switch

switch语句基于不同的条件执行不同的代码块,与if-else系列语句相比,它在处理多个条件时更为清晰和简洁。

switch 表达式 {
case1:// 表达式等于值1时执行的代码
case2,3:// 表达式等于值2或值3时执行的代码
default:// 表达式与所有case都不匹配时执行的代码
}

示例代码

package mainimport "fmt"func main() {grade := "B"switch grade {case "A":fmt.Println("优秀")case "B", "C":fmt.Println("良好")case "D":fmt.Println("及格")default:fmt.Println("不及格")}
}
// 输出:良好
case穿透

特殊的情况:需要多个条件结果的输出,case穿透 。fallthrough
在case中 一旦使用了 fallthrough,则会强制执行下一个case语句,不会去判断条件的。

package mainimport "fmt"func main() {grade := "B"switch grade {case "A":fmt.Println("优秀")case "B", "C":fmt.Println("良好")//在case中 一旦使用了 fallthrough,则会强制执行下一个case语句,不会去判断条件的fallthroughcase "D":fmt.Println("及格")default:fmt.Println("不及格")}
}

image-20240919210628794

可以看到,grade满足case “B”, “C”,走了这个分支后,又走了下面一个分支case “D”
fallthrough实现了case穿透

5) 跳转控制

跳转控制语句用于改变程序的正常执行流程,Go 语言提供了 gotobreakcontinue 语句来实现跳转控制。

goto语句

goto 标签名

...  
标签名:  
// 代码块

示例代码(尽管不推荐,但展示其用法):

package mainimport "fmt"func main() {i := 0
HERE:fmt.Println(i)i++if i < 5 {goto HERE}
}// 输出:从0到4的连续数字

结果

image-20240919212039235

break语句

break : 结束整个循环,立即停止

continue :结束当前这次循环,继续进行下一次循环

package mainimport "fmt"func main() {for i := 1; i <= 10; i++ {if i == 5 {break}fmt.Println(i)}}

i=5的时候退出循环

image-20240919212158817

continue

continue语句用于跳过当前循环的剩余部分,直接进入下一次迭代。同样,continue仅影响最近的一层循环。

package mainimport "fmt"func main() {for i := 1; i <= 10; i++ {if i == 5 {continue // 到这里就结束了当次循环,不会向下了,继续从头开始}fmt.Println(i)}}

结果如下,缺少5

image-20240919212216660

2.8 函数

什么是函数?

  • 函数是基本的代码块,用于执行一个任务,
  • Go 语言最少有个 main() 函数。
  • 你可以通过函数来划分不同功能,逻辑上每个函数执行的是指定的任务。
  • 函数声明告诉了编译器函数的名称,返回类型,和参数。

普通函数声明

函数声明包括函数名、形式参数列表(可省略)、返回值列表(可省略)以及函数体。

注:函数声明时传入的参数叫形参,函数被调用时传入的实际参数叫实参,参数的传递会发生值拷贝(深拷贝),如果形参定义的是指针类型,那就是浅拷贝。

func function_name([parameter list]) [return_types]
{}
  • 无参无返回值函数
  • 有一个参数的函数
  • 有两个参数的函数
  • 有一个返回值的函数
  • 有多个返回值的函数

例子

// 定义无参,无返回值
func hello() {fmt.Println("Hello GoLang")	
}// 调用
hello()

案例

package mainimport "fmt"// 定义有一个参数的函数
func printArr(arr *[5]int) {for i := 0; i < len(arr); i++ {arr[i] = i}
}// 定义两个参数的函数, 返回一个int
func add2(a, b int) int {return a + b
}func main() {// 定义了var arr1 [5]intprintArr(&arr1)fmt.Println(arr1)fmt.Println(add2(1, 2))
}

匿名函数

Go语言支持匿名函数,即在需要使用函数时再定义函数,匿名函数没有函数名只有函数体,函数可以作为一种类型被赋值给函数类型的变量,匿名函数也往往以变量方式传递,这与C语言的回调函数比较类似,不同的是,Go语言支持随时在代码里定义匿名函数。

语法

func(参数列表)(返回参数列表){函数体
}

实例

// 定义匿名函数后,把函数赋值给变量
func main() {f := func(data int) {fmt.Println("hello", data)}// 使用f()调用,相当于 f 就是函数名f(100)
}

实例2

func main() {// 定义匿名函数,后面的 (100) 表示定义后直接调用func(data int) {fmt.Println("hello", data)}(100)
}

闭包

Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:函数 + 引用环境 = 闭包.

语法

func 函数名(形式参数列表) 返回值是匿名函数 {return 匿名函数
}

实例

被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。

func Accumulate(value int) func() int {// 返回一个闭包return func() int {// 累加value++// 返回一个累加值return value}
}
func main() {// 创建一个累加器, 初始值为1accumulator := Accumulate(1)// 第一次调用 值累加fmt.Println(accumulator()) // 2// 第二次调用,值在上一次累加的基础上再累加,并不是重新计算,所以叫有记忆fmt.Println(accumulator()) // 3
}

defer 延迟调用

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,也就是说,先被 defer 的语句最后被执行,最后被 defer 的语句,最先被执行。

关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。

注意点

  • defer一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。类似面向对象编程的语言 Java 和 C# 的 finally 语句块。
  • 如果同一个函数里有多个defer,则后注册的先执行(写在后面的先执行)。
  • defer后可以跟一个func,func内部如果发生panic,会把panic暂时搁置,当把其他defer执行完成后再执行panic。
  • defer后跟一条执行语句,则相关变量在注册defer时被拷贝或计算。

实例

func main() {// 执行顺序 A B C D Efmt.Println("A")defer fmt.Println("E")fmt.Println("B")defer fmt.Println("D")fmt.Println("C")
}

实例2

/*
执行顺序 A B C D
代码解释:
*** 先执行A return 10,表示 返回值变量 a 的值已经从5 变为10了
//
*** 再执行B ,为什么结果会打印5,而不是10呢?上面不是所把5赋值成10了吗? 这里要说的就是相关变量在注册时(编译)就计算好了a的值
*** 编译时 第一行 a = 5, 那B的地方拿到a的值就是5已经计算好了,后面变成10,是在运行时return成为的10
//
*** 再执行C,打印5,原理和B的一样,在编译的时候,已经把a=5把5传给b了已经计算好了,那么我b的值就是5
//
*** 再执行D,打印10,因为return 10是先执行的,这个方法也没有传参赋值的操作,那么这个时候
*** 执行顺序就是 return 10,接着 fmt.Print(a),那么打印的结果就是10
//
总结:可以理解(个人理解)为加了defer的代码,是在本函数执行完后执行的,有传值的函数或者执行语句 那么值就是在当前函数执行前的值,没有传值的函数,就是调用函数执行完后的结果,伪代码如下func test() (a int) {a = 5return 10}func main() {var b = test()fmt.Printf("a1 = %d \n", a)	// 打印 5 // 有执行语句,值是函数执行前的值func(b int) {fmt.Printf("b1=%d \n", b) // 打印 5	// 有传参的函数,值是函数执行前的值}(a)func() {fmt.Printf("a2=%d \n", b) // 打印 10	// 没有传值的函数,值是 函数执行往后的值}()}
*/
func testDefer() (a int) {a = 5// 执行顺序 Ddefer func() {fmt.Printf("a2=%d \n", a) // 打印 10}()// 执行顺序 Cdefer func(b int) {fmt.Printf("b1=%d \n", b) // 打印 5}(a)// 执行顺序 Bdefer fmt.Printf("a1 = %d \n", a) // 打印 5// 执行顺序 Areturn 10 // 表示 a = 10
}func main() {testDefer()
}

运行结果

image-20240919220104104

异常处理

普通异常

Go语言没有类似 Java 或 .NET 中的异常处理机制(try catch),虽然可以使用 defer、panic、recover 模拟,但官方并不主张这样做,Go语言的设计者认为其他语言的异常机制已被过度使用,上层逻辑需要为函数发生的异常付出太多的资源,同时,如果函数使用者觉得错误处理很麻烦而忽略错误,那么程序将在不可预知的时刻崩溃。

Go语言希望开发者将错误处理视为正常开发必须实现的环节,正确地处理每一个可能发生错误的函数,同时,Go语言使用返回值返回错误的机制,也能大幅降低编译器、运行时处理错误的复杂度,让开发者真正地掌握错误的处理。

Go语言的错误处理思想及设计包含以下特征:

  • 一个可能造成错误的函数,需要返回值中返回一个错误接口(error),如果调用是成功的,错误接口将返回 nil,否则返回错误。

  • 在函数调用后需要检查错误,如果发生错误,则进行必要的错误处理。

案例

package mainimport ("errors""fmt"
)// 接收两个int类型,返回它们相除的结果
func testErr(a int, b int) (result int, err error) {if a == 0 || b == 0 {// 这个是0,和一个错误信息return 0, errors.New("除0异常")}// 返回一个正确结果,和一个空return a / b, nil
}func main() {result, err := testErr(12, 0)if err != nil {fmt.Println(err.Error())return}fmt.Println(result)
}
自定义异常
// User 定义一个结构体
type User struct {Id   stringName stringAge  int
}// UserError 自定义一个用户异常
type UserError struct {userId  string // 出异常的用户iderrTime string // 出异常的时间msg     string // 异常消息
}// NewUserError 自定义用户异常消息
func NewUserError(userId, msg string) *UserError {return &UserError{userId:  userId,errTime: time.Now().Format("2006-01-02"),msg:     msg,}
}// 自定义异常必须重写Error()方法
func (e *UserError) Error() string {return "异常时间:" + e.errTime + ",异常用户ID:" + e.userId + ",异常信息:" + e.msg
}// service层 使用 自定义异常
func selectUser() (*User, error) {var user = new(User) // 比如所这个user是从数据库中查询出来的if nil == user {return nil, NewUserError("1", "用户不存在")}return user, nil
}// 使用自定义异常
func main() {user, err := selectUser()if err != nil {fmt.Println(err.Error())return}fmt.Println(*user)
}
panic 宕机

Go语言的类型系统会在编译时捕获很多错误,但有些错误只能在运行时检查,如数组访问越界、空指针引用等,这些运行时错误会引起宕机。

宕机不是一件很好的事情,可能造成体验停止、服务中断,就像没有人希望在取钱时遇到 ATM 机蓝屏一样,但是,如果在损失发生时,程序没有因为宕机而停止,那么用户将会付出更大的代价,这种代价可以是金钱、时间甚至生命,因此,宕机有时也是一种合理的止损方法。

panic 就是在程序可能发生严重异常的地方,主动执行 panic 让程序主动宕机,这样开发者可以及时地发现错误,减少可能造成的损失。

案例

package mainfunc main() {panic("手动宕机") //panic() 的参数可以是任意类型的。
}

panic会执行哪些操作

1:执行当前 goroutine(可以先理解成线程)的 defer 链。

2:随后,程序崩溃并输出日志信息,日志信息包括 panic value 和函数调用的堆栈跟踪信息。

3:调用 exit(2) 结束整个进程。

在宕机时触发延迟执行语句

当 panic() 触发的宕机发生时,panic() 后面的代码将不会被运行,但是在 panic() 函数前面已经运行过的 defer 语句依然会在宕机发生时发生作用。

案例

func main() {defer fmt.Println("宕机后要做的事情2")defer fmt.Println("宕机后要做的事情1")panic("宕机")
}

捕获 panic

func main() {// 使用 recover() 函数可以捕获 panicdefer func() {if err := recover(); err != nil {fmt.Printf("函数中发生了 panic,%s\n", err)}}()panic("宕机")
}

回调函数

在Go语言中,函数回调是一种非常常见且强大的编程技术。函数回调允许我们将一个函数作为参数传递给另一个函数,并在适当的时机由那个函数来调用它。这种机制允许我们编写更加模块化和可复用的代码。

首先,我们需要理解什么是函数回调。简单来说,函数回调就是一个函数在执行过程中调用了另一个函数。被调用的函数称为回调函数。在Go语言中,函数是一等公民,可以作为变量传递,这为实现函数回调提供了便利。

案例

package mainimport "fmt"// 定义一个回调函数类型
type Callback func(int)// 实现一个接受回调函数作为参数的函数
func Process(data int, callback Callback) {// 在这里处理数据...fmt.Println("Processing data:", data)// 调用回调函数callback(data)
}// 定义一个符合回调函数类型的函数
func HandleData(data int) {fmt.Println("Handling data:", data)
}func main() {// 调用Process函数,并传入HandleData作为回调函数Process(10, HandleData)
}

运行结果

Processing data: 10
Handling data: 10

在这个示例中,我们定义了一个Callback类型,它是一个接受一个int类型参数并返回nil的函数。然后,我们实现了一个Process函数,它接受一个int类型的数据和一个Callback类型的回调函数作为参数。在Process函数的内部,我们处理数据并调用回调函数。

我们还定义了一个HandleData函数,它的签名与Callback类型相匹配。在main函数中,我们调用Process函数,并传入10作为数据和HandleData作为回调函数。当Process函数执行到调用回调函数的代码时,它会调用HandleData函数。

函数式编程

2.9 数组

数组在声明(使用 [长度]类型 进行声明)的时候必须指定长度,可以修改数组元素,但是不可修改数组长度。

Golang Array和以往认知的数组有很大不同:

  1. 数组:是同一种数据类型的固定长度的序列。

  2. 数组定义:var name [len]type,比如:var a [5]int,数组长度必须是常量,且是类型的组成部分。一旦定义,长度不能变。

  3. 长度是数组类型的一部分,因此,var a [5]intvar a [10]int是不同的类型。

  4. 数组可以通过下标进行访问,下标是从0开始,最后一个元素下标是:len-1

    for i := 0; i < len(a); i++ {
    }
    for index, v := range a {
    }
    
  5. 访问越界,如果下标在数组合法范围之外,则触发访问越界,会panic

  6. 数组是 值类型 ,赋值和传参会复制整个数组,而不是指针。因此改变副本的值,不会改变本身的值。(与 C 数组变量隐式作为指针使用不同,Go 数组是值类型,赋值和函数传参操作都会复制整个数组数据。)

  7. 支持 “==”、“!=” 操作符,因为内存总是被初始化过的。

  8. 指针数组 [n]*T,数组指针 *[n]T

声明

Go 声明数组需要指定元素类型及元素个数,语法格式如下:

var name [SIZE]type

以上为一维数组的定义方式。
多维数组的声明:

var name [SIZE1][SIZE2]...[SIZEN]type

初始化

var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

初始化操作

//一维数组的声明
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
//多维数组的声明
a := [3][4]int{  {0, 1, 2, 3} ,   /*  第一行索引为 0 */{4, 5, 6, 7} ,   /*  第二行索引为 1 */{8, 9, 10, 11}}   /* 第三行索引为 2 */

特殊情况的说明

数组长度不确定
如果数组长度不确定,可以使用 ... 代替数组的长度,编译器会根据元素个数自行推断数组的长度

var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
或
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

只初始化个别元素(通过下标)
如果设置了数组的长度,我们还可以通过指定下标来初始化元素:

//  将索引为 1 和 3 的元素初始化
balance := [5]float32{1:2.0,3:7.0}

初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大

总结

全局变量:
var arr0 [5]int = [5]int{1, 2, 3}
var arr1 = [5]int{1, 2, 3, 4, 5}
var arr2 = [...]int{1, 2, 3, 4, 5, 6}
var str = [5]string{3: "hello world", 4: "tom"}
局部变量:
a := [3]int{1, 2}           // 未初始化元素值为 0。
b := [...]int{1, 2, 3, 4}   // 通过初始化值确定数组长度。
c := [5]int{2: 100, 4: 200} // 使用索引号初始化元素。
d := [...]struct {name stringage  uint8
}{{"user1", 10}, // 可省略元素类型。{"user2", 20}, // 别忘了最后一行的逗号。
}

多维数组方法

全局变量:
var arr0 [5][3]int
var arr1 [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}
局部变量:
a := [2][3]int{{1, 2, 3}, {4, 5, 6}}
b := [...][2]int{{1, 1}, {2, 2}, {3, 3}} // 第 2 纬度不能用 "..."。

访问

var salary float32 = balance[9]

遍历

package mainimport ("fmt"
)func main() {var f [2][3]int = [...][3]int{{1, 2, 3}, {7, 8, 9}}for k1, v1 := range f {for k2, v2 := range v1 {fmt.Printf("(%d,%d)=%d ", k1, k2, v2)}fmt.Println()}
}

获得数组长度

内置函数 len 和 cap 都返回数组长度 (元素数量)。

package mainfunc main() {a := [2]int{}println(len(a), cap(a))
}

向函数传递数组

如果你想向函数传递 数组参数,你需要在函数定义时,声明形参为数组,我们可以通过以下两种方式来声明:

方式一:形参设定数组大小:

void myFunction(param [10]int)
{
.
.
.
}

方式二:形参未设定数组大小:

void myFunction(param []int)
{
.
.
.
}

实例:实例:函数接收整型数组参数,另一个参数指定了数组元素的个数,并返回平均值:

package mainimport "fmt"func main() {/* 数组长度为 5 */var  balance = [5]int {1000, 2, 3, 17, 50}var avg float32/* 数组作为参数传递给函数 */avg = getAverage( balance, 5 ) ;/* 输出返回的平均值 */fmt.Printf( "平均值为: %f ", avg );
}func getAverage(arr [5]int, size int) float32 {var i,sum intvar avg float32  for i = 0; i < size;i++ {sum += arr[i]}//提升精度avg = float32(sum) / float32(size)return avg;
}

使用数组指针传参

package mainimport "fmt"func printArr(arr *[5]int) {arr[0] = 10for i, v := range arr {fmt.Println(i, v)}
}func main() {var arr1 [5]intprintArr(&arr1)fmt.Println(arr1)arr2 := [...]int{2, 4, 6, 8, 10}printArr(&arr2)fmt.Println(arr2)
}

总结:想通过函数调用改变数组内容的两种方式

  1. return + 赋值
  2. 用 slice,或数组指针来传参(推荐)

2.10 结构体

什么是结构体

  • 结构体是一种自定义的数据类型,用于表示一组相关的数据字段。
  • 结构体可以包含任意数量和类型的字段,每个字段都有一个名称和一个类型。
  • 结构体的定义使用关键字 typestruct

定义

type 结构体名 struct {字段1 数据类型1字段2 数据类型2...
}

举个例子

type Class struct{id intname stringage intcredit int
}

初始化

package mainimport ("fmt"
)type Class struct {id     intname   stringage    intcredit int
}func main() {// 第一种:按照顺序初始化var zhangsan Class = Class{1, "zhangsan", 18, 100}// 第二种:部分初始化var lisi Class = Class{name: "lisi", age: 20}// 第三种: 结构体.成员 方式初始化var wangwu Classwangwu.id = 3wangwu.name = "wangwu"wangwu.age = 23// 第四种:自动推导类型初始化maliu := Class{4, "maliu", 18, 90}fmt.Printf("%v\n%v\n%v\n%v\n", zhangsan, lisi, wangwu, maliu)}

结构体与数组

package mainimport "fmt"type Class struct {id     intname   stringage    intcredit int
}func main() {var arr [3]Class = [3]Class{{1, "zhangsan", 18, 200},{2, "lisi", 19, 100},{3, "wagnwu", 22, 300},}// 修改结构体成员值arr[0].age = 666// 输出整个数组fmt.Println(arr)// 输出下标0的名称fmt.Println(arr[0].name)// 输出下标0的年龄fmt.Println(arr[0].age)}

代码输出内容

[{1 zhangsan 666 200} {2 lisi 19 100} {3 wagnwu 22 300}]
zhangsan
666

切片

package mainimport "fmt"type Class struct {id     intname   stringage    intcredit int
}func main() {// 定义切片使用Class类型var s []Class = []Class{{101, "zhangsan", 10, 20},{102, "lisi", 20, 30},{103, "wangwu", 30, 100},}// 修改值s[0].age = 666// 末尾追加元素s = append(s, Class{104, "maliu", 28, 100})// 遍历切片for i := 0; i < len(s); i++ {fmt.Println(s[i])}
}

*号 和 &号

变量是什么?变量是一块内存的别名,即给这块内存起一个名字,这块内存在计算机内存中有具体的地址。

&:取地址符号,表示取出这个变量所在内存的地址。

*:指针符号,表示指向变量的内存地址。指针变量就是指向内存地址的变量

*是指,&是取,可以理解成 &取得的值赋给*,即 *p = &a

一个变量由变量地址和地址上的数据组成,在内存中物理表示的方式如下图

image-20240919214405569

var a = 10
var p *int                   // 第 2 行
p = &afmt.Printf("%T \n", p) // *intfmt.Println(&a) // 0xc00001e0a8
fmt.Println(p)  // 0xc00001e0a8fmt.Println(a)   // 10
fmt.Println(*&a) // 10        // &a = p,那么是不是 *&a 和 *p 是一样的
fmt.Println(*p)  // 10        // 第 10 行// 注意第2行和第10行*号的区别,第2行表示这是一个指针变量,第10行表示取这个指针地址上的数据

构造函数

Go语言的类型或结构体没有构造函数的功能,可以使用结构体初始化的过程来模拟实现构造函数。

实例

// Cat 定义结构体
type Cat struct {Color stringName  string
}// NewCatByName 定义构造函数,只初始化名称
func NewCatByName(name string) *Cat {return &Cat{Name: name,}
}// NewCatByColor 定义构造函数,只初始化颜色
func NewCatByColor(color string) *Cat {return &Cat{Color: color,}
}// 调用构造函数,生成构造实例
var c = NewCatByName("大花")
fmt.Println(c.Name)

匿名成员变量

匿名字段,直接使用类型作为字段名,所以匿名字段中不能出现重复的数据类型。

实例

// 定义
type Student struct {id      intstring  // 匿名字段float32 // 匿名字段
}// 使用
var stu = Student{id: 1, string: "ZhangSan", float32: 88.3}
fmt.Println(stu.string)

方法传值

  • 方法入参是普通的结构体时,在函数里修改入参结构体对象不影响原来的结构体数据。因为传入的是数据的拷贝,不影响原来的数据。(深拷贝)
  • 方法入参是指针类型的结构体时,在函数里修改入参结构体对象时,会影响原来的结构体数据,因为传入的是指针,修改会直接修改指针地址上的数据。(浅拷贝)
func hello(u *user) {u.username = "LiSi"
}func (u *user) hello() {u.username = "LiSi"
}

结构体与tag

在定义结构体时,可以给每个字段加上一个tag

type Student struct {Name string "tag-name"Age int "tag-age"
}

结构体的tag可以通过反射机制获取,常见用于序列化和反序列化

比如

type JsonResult struct {Code  int         `json:"code"`Msg   interface{} `json:"msg"`Data  interface{} `json:"data"`Count int         `json:"count"`
}

案例

package mainimport "fmt"
import "encoding/json"type JsonResult struct {Code  int         `json:"code"`Msg   interface{} `json:"msg"`Data  interface{} `json:"data"`Count int         `json:"count"`
}func main() {fmt.Println("hello world")var a JsonResult = JsonResult{Code:  200,Msg:   "ok",Data:  "hello world",Count: 1,}jsonBytes, err := json.Marshal(a)if err != nil {fmt.Println("JSON 序列化失败:", err)return}fmt.Println("JSON 字符串:", string(jsonBytes))
}

image-20240921142607870

结构体中方法的定义与使用

  • 方法类似于函数,只不过方法可以进行绑定,方法可以绑定到指定的数据类型上,使该数据类型的实例都可以使用这个方法
  • 方法不能独自调用,只能通过绑定的数据类型的实例进行调用
  • 自定义类型都可以有方法,不仅仅是struct
type A struct {Name string
}func (a A) test() {fmt.Println(a.Name)
}func main() {a := A{Name:"111"}a.test()
}
  • 接收者必须有一个显式的名字,这个名字必须在方法中被使用

  • 如果方法不需要使用接收者的值,可以用 _ 替换它

  • 接收者必须有一个显式的名字,这个名字必须在方法中被使用

  • 如果方法不需要使用接收者的值,可以用 _ 替换它

func (_ receiver_type) methodName(parameter_list) (return_value_list) { ... }
  • 类型和作用在它上面定义的方法必须在同一个包里定义,这就是为什么不能在 int、float 或类似这些的类型上定义方法,可以为当前包中任何类型定义方法,必须是当前包

  • Go中的toString()方法

    func (a type) String() string { ... }
    

2.11 接口

什么是接口?

在Go语言中,接口(interface)是方法的集合,它允许我们定义一组方法但不实现它们,任何类型只要实现了这些方法,就被认为是实现了该接口。

接口更重要的作用在于多态实现,它允许程序以多态的方式处理不同类型的值。接口体现了程序设计的多态和高内聚、低耦合的思想。

package mainimport "fmt"// 定义接口
type Person interface {GetName() stringGetAge() int
}// 定义结构体
type Student struct {Name stringAge  int
}// 实现接口
// 实现 GetName 方法
func (s Student) GetName() string {fmt.Println("name:", s.Name)return s.Name
}// 实现 GetAge 方法
func (s Student) GetAge() int {fmt.Println("age:", s.Age)return s.Age
}// 使用接口
func main() {var per Personvar stu Studentstu.Name = "xiaozhang"stu.Age = 24per = stuper.GetName()  // name: xiaozhangper.GetAge()  // age: 24
}

注意事项

  • (1)接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量(实例)。

  • (2)接口中所有的方法都没有方法体,即都是没有实现的方法。

  • (3)在Go中,一个自定义类型需要将某个接口的所有方法都实现,才能说这个自定义类型实现了该接口。

  • (4)一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型。

  • (5)只要是自定义数据类型就可以实现接口,不仅仅是结构体类型(structs),还包括类型别名(type aliases)、其他接口、自定义类型、变量等。

  • (6)一个自定义类型可以实现多个接口。

  • (7)interface 接口不能包含任何变量。

  • (8)一个接口可以继承多个别的接口,这时如果要实现这个接口必须实现它继承的所有接口的方法。

    在低版本的Go编辑器中,一个接口继承其他多个接口时,不允许继承的接口有相同的方法名。比如A接口继承B、C接口,B、C接口的方法名不能一样。高版本的Go编辑器没有相关问题。

  • (9)interface类型默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil。

    var i interface{}  
    fmt.Println(i == nil) // 输出:true
    
  • (10)在Go中,接口的实现是非侵入式,隐式的,不需要显式声明“我实现了这个接口”。只要一个类型提供了接口中定义的所有方法的具体实现,它就自动成为该接口的一个实现者。

  • (11)空接口interface{}没有任何方法,是一个能装入任意数量、任意数据类型的数据容器,我们可以把任何一个变量赋给空接口类型。任意数据类型都能实现空接口,这就和 “空集能被任意集合包含” 一样,空接口能被任意数据类型实现。

底层实现

Go的interface源码在Golang源码的runtime目录中。Go的interface是由两种类型来实现的:iface和eface。runtime.iface表示非空接口类型,runtime.eface表示空接口类型interface{}。

iface是包含方法的interface,如:

type Person interface {GetName()
}

eface是不包含方法的interface,即空interface,如:

type Person interface {
}
//或者
var person interface{} = xxxx实体

iface的源代码是:

type iface struct {tab  *itab  // 表示值的具体类型的类型描述符data unsafe.Pointer  // 指向值的指针(实际的数据)
}

itab是iface不同于eface的关键数据结构。其包含两部分:一部分是唯一确定包含该interface的具体结构类型,一部分是指向具体方法集的指针。

参考资料

GO语言中的接口(interface)_go 接口-CSDN博客

3. 高级

3.1 IO流使用

3.2 泛型

参考资料

[Go 函数-CSDN博客](https://blog.csdn.net/a1053765496/article/details/129842600

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/887393.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数据库的联合查询

数据库的联合查询 简介为什么要使⽤联合查询多表联合查询时MYSQL内部是如何进⾏计算的构造练习案例数据案例&#xff1a;⼀个完整的联合查询的过程 内连接语法⽰例 外连接语法 ⽰例⾃连接应⽤场景示例表连接练习 ⼦查询语法单⾏⼦查询多⾏⼦查询多列⼦查询在from⼦句中使⽤⼦查…

LeetCode-632. Smallest Range Covering Elements from K Lists [C++][Java]

目录 题目描述 解题思路 【C】 【Java】 LeetCode-632. Smallest Range Covering Elements from K Listshttps://leetcode.com/problems/smallest-range-covering-elements-from-k-lists/description/ 题目描述 You have k lists of sorted integers in non-decreasing o…

UI自动化测试中公认最佳的设计模式-POM

一、概念 什么是POM&#xff1f; POM是PageObjectModule&#xff08;页面对象模式&#xff09;的缩写&#xff0c;其目的是为了Web UI测试创建对象库。在这种模式下&#xff0c;应用涉及的每一个页面应该定义为一个单独的类。类中应该包含此页面上的页面元素对象和处理这些元…

Scala文件读写——成绩分析

根据文件解决下例问题 1.读入txt文件&#xff1a;按行读入 import scala.io.Sourceobject Test文件读写_成绩分析 {def main(args: Array[String]): Unit {//1.按行读入val source Source.fromFile("score.txt")val it source.getLines()it.next()//跳过第一行wh…

C# Winform 俄罗斯方块小游戏源码

文章目录 1.设计来源俄罗斯方块小游戏讲解1.1 主界面1.2 游戏界面1.3 游戏结束界面1.4 配置游戏界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;https:…

前端框架Vue3——响应式数据,v-on,v-show和v-if,v-for,v-bind

Vue的定义为渐进式的JavaScript框架。所谓渐进式&#xff0c;是指其被设计 为可以自底向上逐层应用。我们可以只使用Vue框架中提供的某层的功 能&#xff0c;也可以与其他第三方库整合使用。当然&#xff0c;Vue本身也提供了完整的 工具链&#xff0c;使用其全套功能进行项目的…

实验二 系统响应及系统稳定性

实验目的 &#xff08;1&#xff09;学会运用Matlab 求解离散时间系统的零状态响应&#xff1b; &#xff08;2&#xff09;学会运用Matlab 求解离散时间系统的单位取样响应&#xff1b; &#xff08;3&#xff09;学会运用Matlab 求解离散时间系统的卷积和。 实验原理及实…

.NET Core发布网站报错 HTTP Error 500.31

报错如图&#xff1a; 解决办法&#xff1a; 打开任务管理器》》服务》》找到这仨服务&#xff0c;右键启动即可&#xff0c;如果已经启动了就重启&#xff1a;

麒麟安全增强-kysec

DAC: 自主访问控制是linux下默认的接入控制机制,通过对资源读、写、执行操作,保证系统安全 MAC:安全接入控制机制,由操作系统约束的访问控制,默认情况下,MAC不允许任何访问,用户可以自定义策略规则制定允许什么 ,从而避免很多攻击。 MAC强制访问控制常见的实现方式:…

Otter 安装流程

优质博文&#xff1a;IT-BLOG-CN 一、背景 随着公司的发展&#xff0c;订单库的数据目前已达到千万级别&#xff0c;需要进行分表分库&#xff0c;就需要对数据进行迁移&#xff0c;我们使用了otter&#xff0c;这里简单整理下&#xff0c;otter 的安装过程&#xff0c;希望对…

如何解决Jupyter command `jupyter-contrib` not found.

目录 (base) C:\Users\hello>pip show jupyter_contrib_nbextensions Name: jupyter-contrib-nbextensions Version: 0.7.0 Summary: A collection of Jupyter nbextensions. Home-page: https://github.com/ipython-contrib/jupyter_contrib_nbextensions.git Author: ipyt…

Gitee markdown 使用方法(持续更新)

IPKISS 获取仿真器件的名称 引言正文标题换行第一种------在行末尾手动键入两个空格第二种------额外换行一次&#xff0c;即两行中间留一个空行 缩进与反缩进代码块行内代码添加图片添加超链接 加粗&#xff0c;倾斜&#xff0c;加粗倾斜 引言 有些保密性的文件或者教程&…

Adobe Illustrator 2024 安装教程与下载分享

介绍一下 下载直接看文章末尾 Adobe Illustrator 是一款由Adobe Systems开发的矢量图形编辑软件。它广泛应用于创建和编辑矢量图形、插图、徽标、图标、排版和广告等领域。以下是Adobe Illustrator的一些主要特点和功能&#xff1a; 矢量绘图&#xff1a;Illustrator使用矢量…

golang学习5

为结构体添加方法 异常处理过程

分布式光伏与储能协调控制策略的应用分析

安科瑞汪洋/汪小姐/汪女士---Acrelwy 摘 要&#xff1a;针对光伏发电的随机性、波动性、间歇性特征,研发了分布式光伏与储能协调控制策略,并在镇江地调开展分布式电源光储协控试点应用&#xff0c;开展光储协调控制策略研究&#xff0c;实时采集分布式光伏电站、储能电站、试点…

sd webui整合包怎么安装comfyui

环境: sd webui整合包 comfyui 问题描述: sd webui整合包怎么安装comfyui 扩展安装不成功 解决方案: 1.直接下载 ,解压到SD文件夹里(或者git拉一下) 2.ComfyUI模型共享:如果本机部署过Webui,那么ComfyUI可以与WebUI公用一套模型,防止复制大量模型浪费空间 将…

Utf8Json 枚举序列化为整型(默认string)

Utf8Json 枚举序列化为整型 找到 StandardResolver.cs, 更换EnumResolver. Default 为EnumResolver. UnderlyingValue

网络基础 - 地址篇

一、IP 地址 IP 协议有两个版本&#xff0c;IPv4 和 IPv6IP 地址(IPv4 地址)是一个 4 字节&#xff0c;32 位的正整数&#xff0c;通常使用 “点分十进制” 的字符串进行表示&#xff0c;例如 192.168.0.1&#xff0c;用点分割的每一个数字表示一个字节&#xff0c;范围是 0 ~…

Vue3+SpringBoot3+Sa-Token+Redis+mysql8通用权限系统

sa-token支持分布式token 前后端代码&#xff0c;地球号: bright12389

Leetcode 336 回文对

示例 1&#xff1a; 输入&#xff1a;words ["abcd","dcba","lls","s","sssll"] 输出&#xff1a;[[0,1],[1,0],[3,2],[2,4]] 解释&#xff1a;可拼接成的回文串为 ["dcbaabcd","abcddcba","sl…