Golang基础

一、Go语言

0.Go前景

  • 云计算基础设施领域
    代表项目:docker、kubernetes、etcd、consul、cloudflare CDN、七牛云存储等。
  • 基础软件
    代表项目:tidb、influxdb、cockroachdb 等。
  • 微服务
    代表项目:go-kit、micro、monzo bank 的 typhon、bilibili 等。
  • 互联网基础设施
    代表项目:以太坊、hyperledger 等。

Go框架组件:

Web
beego 国内开发,文档比较全
gin 主流web框架,轻量级,高性能
echo 类似于gin
Iris 重量级,高性能
微服务
go kit 一套集成组合方案,包含很多工具,比较灵活。
Istio 一体化集成式框架,包括熔断降级、安全审计等一整套方案,适合比较繁琐的大型微服务项目。
Tcp长链接
zinx 轻量级服务器
容器编排
kubernetes google研发
swarm docker团队研发
服务发现
consul 服务发现与注册
存储引擎
etcd k/v存储,类似于redis,支持分布式存储
tidb 国内开发,分布式sql存储
静态建站
hugo 轻松创建门户网站
中间件
nsq 消息队列
leaf 游戏服务器
grpc RPC框架,主要功能远程调度
codis redis集群
爬虫 goquery

1.Go基础

安装:源码、标准包(推荐)、三方工具。如果想在一个系统安装多版本Go,推荐GVM。

1.1 Go组成

Go 语言的基础组成有以下几个部分:

1
2
3
4
5
6
7
package main  //包声明  每个Go应用程序都包含一个名为 main 的包,main方法必须在main包中运行

import "fmt" //引入包 fmt 包实现了格式化 IO(输入/输出)的函数 也可以导入网址包

func main() { //函数
   fmt.Println("Hello, World!") //语句 & 表达式
}
1
2
$ go run xxx.go   //运行以上代码的命令
需要注意的是 { 不能单独放在一行

行分隔符:在 Go 程序中,一行代表一个语句结束。不需要在语句或者声明的末尾添加分号,除非一行上有多条语句。

字符串连接:通过 + 实现

1
2
3
4
5
6
package main
import "fmt"
func main() {
fmt.Println("Google" + "Runoob")
}
输出结果:GoogleRunoob

go语言关键字:

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

1.2 Go命令

1
2
go get 动态获取远程包(github) 	// 第一步是下载源码包,第二步是执行go install
go get -u 自动更新包
1
2
3
4
5
6
7
8
$ go run helloworld.go //编译一个或多个以.go结尾的源文件,链接库文件,生成可执行文件helloworld.exe
$ go version //查看当前go语言版本
$ go build helloworld.go //编译,同时保存编译结果以备将来之用,会忽略目录下以“_”或“.”开头的go文件。
$ go clean // 用来移除当前源码包和关联源码包里面编译生成的文件。
$ go fmt // 格式化你写好的代码文件
$ go install // 第一步是生成结果文件(可执行文件或者.a包),第二步会把编译好的结果移到$GOPATH/pkg或者$GOPATH/bin。
$ go test // 会自动读取源码目录下面名为*_test.go的文件,生成并运行测试用的可执行文件。
$ go env // 查看当前go的环境变量
1
2
3
go tool:
go tool fix . // 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1,例如API的变化
go tool vet directory|files // 用来分析当前目录的代码是否都是正确的代码,例如是不是调用fmt.Printf里面的参数不正确,例如函数里面提前return了然后出现了无用代码之类的。

Go语言原生支持Unicode,它可以处理全世界任何语言的文本。


自动添加import声明:

goimports,可以根据代码需要,自动地添加或删除import声明。这个工具并没有包含在标准的分发包中,可以用下面的命令安装:

1
$ go get golang.org/x/tools/cmd/goimports

1.3 命令行参数

os包以跨平台的方式,提供了一些与操作系统交互的函数和变量。

程序的命令行参数可从os包的Args变量获取;os包外部使用os.Args访问该变量。

使用一条短变量声明来声明并初始化s

1
2
3
4
5
// 以下四条等同
s := "" // 推荐
var s string // 推荐
var s = ""
var s string = ""

1.4 类型

类型转换

假设x为int类型,将x转为byte类型:

1
byte(x)

标准库strconv可在不同进制(字符串)间转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
import "strconv"

func main() {
a, _ := strconv.ParseInt("1100100", 2, 32) // 将“1100100”作为二进制转换为32位int
b, _ := strconv.ParseInt("0144", 8, 32)
c, _ := strconv.ParseInt("64", 16, 32)

println(a, b, c) //十进制

println("0b" + strconv.FormatInt(a, 2)) //将 a 转换为二进制字符串,并加上前缀 0b。
println("0" + strconv.FormatInt(a, 8)) //将 a 转换为八进制字符串,并加上前缀 0。
println("0x" + strconv.FormatInt(a, 16)) //将 a 转换为十六进制字符串,并加上前缀 0x。
}

输出:

1
2
3
4
100 100 100
0b1100100
0144
0x64

引用类型

引用类型(reference type)特指slice、map、channel这三种预定义类型。

相比数字、数组等类型,引用类型拥有更复杂的存储结构。除分配内存外,它们还须初始化一系列属性,诸如指针、长度,甚至包括哈希分布、数据队列等。

内置函数new按指定类型长度分配零值内存,返回指针,并不关心类型内部构造和初始化方式。而引用类型则必须使用make函数创建,编译器会将make转换为目标类型专用的创建函数(或指令),以确保完成全部内存分配和相关属性初始化。


2.Go 语法

2.1 变量

运行时内存分配操作会确保变量自动初始化为二进制零值(zero value),避免出现不可预测行为

声明变量的一般形式是使用 var 关键字:

1
2
3
4
var identifier type
ex:var b int
var c bool
// 初始化默认字符值为0

可以一次声明多个变量:

1
var identifier1, identifier2 type

:= 是一个简短变量声明和初始化的操作符:

一行代码中声明一个新的变量,并同时给它赋值;

但是,这种不带声明格式的只能在函数体中出现!!!

注意!!!:如果变量已经存在并且在同一作用域内,使用:=会导致编译错误,因为它会尝试声明一个已存在的变量。另一种情况,当变量在函数体外被全局声明,再次在函数体内声明时会覆盖之前的变量值。

1
2
3
4
intVal := 1
相等于:
var intVal int
intVal =1

四种声明方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//类型相同多个变量, 非全局变量
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断

vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误


// 这种因式分解关键字的写法一般用于声明全局变量
var (
vname1 v_type1
vname2 v_type2
)

交换两个变量的值

1
2
a, b = b, a
// 两个变量的类型必须是相同。

2.2 常量

常量的定义格式:

1
2
3
const identifier [type] = value
const b string = "abc" //显式
const b = "abc" // 隐式

优雅的常量 iota

自增长:

1
2
3
4
5
6
7
const (
CategoryBooks = iota // 0
CategoryHealth // 1
CategoryClothing // 2
)
// 每行iota会累加1,第一行的iota的默认值为0
// 只能在const里使用

2.3 控制和函数

2.3.1 流程控制

if有一个强大的地方就是条件判断语句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内。

1
2
3
4
5
6
7
8
// 计算获取值x,然后根据x返回的大小,判断是否大于10。
if x := computedValue(); x > 10 {
fmt.Println("x is greater than 10")
} else {
fmt.Println("x is less than 10")
}
//如果像下面这样调用输出就编译出错了,因为x是条件里面的变量
fmt.Println(x)

goto跳转到必须在当前函数内定义的标签。

1
2
3
4
5
6
7
8
func myFunc() {
i := 0
Here: //这行的第一个词,以冒号结束作为标签
println(i)
i++
goto Here //跳转到Here去
}
// 这是一个死循环

_下划线:

由于 Go 支持 “多值返回”, 而对于“声明而未被调用”的变量, 编译器会报错, 在这种情况下, 可以使用_来丢弃不需要的返回值。

1
2
3
for _, v := range map{
fmt.Println("map's val:", v)
}

switch

Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch, 但是可以使用fallthrough强制执行后面的case代码。


2.3.2 函数

通过关键字func来声明。

1
2
3
4
5
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
//这里是处理逻辑代码
//返回多个值
return value1, value2
}
  • 关键字func用来声明一个函数funcName

  • 函数可以有一个或者多个参数,每个参数后面带有类型,通过,分隔

  • 函数可以返回多个值

  • 上面返回值声明了两个变量output1output2,如果你不想声明也可以,直接就两个类型

  • 如果只有一个返回值且不声明返回值变量,那么你可以省略 包括返回值 的括号

  • 如果没有返回值,那么就直接省略最后的返回信息

  • 如果有返回值, 那么必须在函数的外层添加return语句

如果函数有两个参数,它们的类型都是int,那么第一个变量的类型可以省略(即 a,b int,而非 a int, b int),默认为离它最近的类型


i. 返回多个值:

Go语言比C更先进的特性,其中一点就是函数能够返回多个值。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main
import "fmt"
//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}
func main() {
x := 3
y := 4
xPLUSy, xTIMESy := SumAndProduct(x, y)
fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子直接返回了两个参数,当然也可以命名返回参数的变量,这样提高生成的文档可读性。

改成如下这样的定义,然后返回的时候不用带上变量名,因为直接在函数里面初始化了。

1
2
3
4
5
func SumAndProduct(A, B int) (add int, Multiplied int) {
add = A+B
Multiplied = A*B
return
}

ii. 变参:

接受变参的函数是有着不定数量的参数的。为了做到这点,首先需要定义函数使其接受变参:

1
func myfunc(arg ...int) {}

arg ...int告诉Go这个函数接受不定数量的参数。注意,这些参数的类型全部是int。在函数体中,变量arg是一个intslice

1
2
3
for _, n := range arg {
fmt.Printf("And the number is: %d\n", n)
}

iii. 传值与传指针

当我们传一个参数值到被调用函数里面时,实际上是传了这个值的一份copy,当在被调用函数中修改参数值的时候,调用函数中相应实参不会发生任何变化,因为数值变化只作用在copy上。

需要传这个参数本身,该怎么办呢?

这就牵扯到了所谓的指针。变量在内存中是存放于一定地址上的,修改变量实际是修改变量地址处的内存。只有add1函数知道x变量所在的地址,才能修改x变量的值。所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值。此时参数仍然是按copy传递的,只是copy的是一个指针。

1
2
3
4
5
6
7
8
9
10
11
12
//简单的一个函数,实现了参数+1的操作
func add1(a *int) int { // 请注意,
*a = *a+1 // 修改了a的值
return *a // 返回新值
}
func main() {
x := 3
fmt.Println("x = ", x) // 应该输出 "x = 3"
x1 := add1(&x) // 调用 add1(&x) 传x的地址
fmt.Println("x+1 = ", x1) // 应该输出 "x+1 = 4"
fmt.Println("x = ", x) // 应该输出 "x = 4"
}

优点:

  • 传指针使得多个函数能操作同一个对象。
  • 传指针比较轻量级 (8bytes),只是传内存地址,我们可以用指针传递体积大的结构体。如果用参数值传递的话, 在每次copy上面就会花费相对较多的系统开销(内存和时间)。所以当你要传递大的结构体的时候,用指针是一个明智的选择。
  • Go语言中channelslicemap这三种类型的实现机制类似指针,所以可以直接传递,而不用取地址后传递指针。(注:若函数需改变slice的长度,则仍需要取地址传递指针)

iv. Defer:

defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。

在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。

特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。

没有defer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func ReadWrite() bool {
file.Open("file")
// 做一些工作
if failureX {
file.Close()
return false
}
if failureY {
file.Close()
return false
}
file.Close()
return true
}
// 可以看出,没有defer需要做一系列操作,需要防止失败等等

有defer:

1
2
3
4
5
6
7
8
9
10
11
12
func ReadWrite() bool {
file.Open("file")
defer file.Close()
if failureX {
return false
}
if failureY {
return false
}
return true
}
// 简洁

defer作用:

  • 释放占用的资源
  • 捕捉处理异常
  • 输出日志

结果

如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func Demo(){
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
defer fmt.Println("4")
}
func main() {
Demo()
}
运行结果:
4
3
2
1

★return先被调用、defer后被调用;即return之后的语句先执行,defer之后的语句后执行。


v. 函数作为值、类型

1
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

把这个类型的函数当做值来传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
type testInt func(int) bool // 声明了一个函数类型,类型名为testInt,输入类型为int,返回类型为bool
func isOdd(integer int) bool {
if integer%2 == 0 {
return false
}
return true
}
func isEven(integer int) bool {
if integer%2 == 0 {
return true
}
return false
}
// 声明的函数类型在这个地方当做了一个参数
func filter(slice []int, f testInt) []int {
var result []int
for _, value := range slice {
if f(value) {
result = append(result, value)
}
}
return result
}
func main(){
slice := []int {1, 2, 3, 4, 5, 7}
fmt.Println("slice = ", slice)
odd := filter(slice, isOdd) // 函数当做值来传递了
fmt.Println("Odd elements of slice are: ", odd)
even := filter(slice, isEven) // 函数当做值来传递了
fmt.Println("Even elements of slice are: ", even)
}

函数当做值和类型在我们写一些通用接口的时候非常有用,通过上面例子我们看到testInt这个类型是一个函数类型,然后两个filter函数的参数和返回值与testInt类型是一样的,但是我们可以实现很多种的逻辑,这样使得我们的程序变得非常的灵活。


vi. Panic和Recover

Go没有像Java那样的异常机制,它不能抛出异常,是使用了panicrecover机制。一定要记住,你应当把它作为最后的手段来使用。

Panic(恐慌)

是一个内建函数,可以中断原有的控制流程,进入一个令人恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panicgoroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。

Recover

是一个内建的函数,可以让进入令人恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。

1
2
3
4
5
6
var user = os.Getenv("USER")
func init() {
if user == "" {
panic("no value for $USER")
}
}

下面这个函数检查作为其参数的函数在执行时是否会产生panic

1
2
3
4
5
6
7
8
9
func throwsPanic(f func()) (b bool) {
defer func() {
if x := recover(); x != nil {
b = true
}
}()
f() //执行函数f,如果f中出现了panic,那么就可以恢复回来
return
}

vii. main函数和init函数

一张图全部解析。

main函数引入包初始化流程图:

img


viii. import

  1. 点操作

我们有时候会看到如下的方式导入包

1
2
3
import(    
. "fmt"
)

这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println(“hello world”)可以省略的写成Println(“hello world”)

  1. 别名操作

别名操作顾名思义我们可以把包命名成另一个我们用起来容易记忆的名字

1
2
3
import(    
f "fmt"
)

别名操作的话调用包函数时前缀变成了我们的前缀,即f.Println(“hello world”)

  1. _操作

这个操作经常是让很多人费解的一个操作符,请看下面这个import

1
2
3
4
 import (     
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)

_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。


2.4 slice和map

Go 语言切片是对数组的抽象。

2.5.1 slice

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),即slice,但实际slice是一个引用类型。与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

定义切片:

你可以声明一个未指定大小的数组来定义切片:

1
var identifier []int

slice:

1
s :=[]int{1,2,3 } // 切片初始化。 数组中字符要加'',数字不用

slice可以从一个数组或一个已经存在的slice中再次声明。slice通过array[i:j]来获取,其中i是数组的开始位置,j是结束位置,但不包含array[j],它的长度是j-i

1
2
3
4
5
6
7
8
9
10
11
12
slice := []byte {'a', 'b', 'c', 'd'}

// 声明一个含有10个元素元素类型为byte的数组
var ar = [10]byte {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个含有byte的slice
var a, b []byte
// a指向数组的第3个元素开始,并到第五个元素结束,
a = ar[2:5]
//现在a含有的元素: ar[2]、ar[3]和ar[4]
// b是数组ar的另一个slice
b = ar[3:5]
// b的元素是:ar[3]和ar[4]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 声明一个数组
var array = [10]byte{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}
// 声明两个slice
var aSlice, bSlice []byte
// 演示一些简便操作
aSlice = array[:3] // 等价于aSlice = array[0:3] aSlice包含元素: a,b,c
aSlice = array[5:] // 等价于aSlice = array[5:10] aSlice包含元素: f,g,h,i,j
aSlice = array[:] // 等价于aSlice = array[0:10] 这样aSlice包含了全部的元素
// 从slice中获取slice
aSlice = array[3:7] // aSlice包含元素: d,e,f,g,len=4,cap=7
bSlice = aSlice[1:3] // bSlice 包含aSlice[1], aSlice[2] 也就是含有: e,f
bSlice = aSlice[:3] // bSlice 包含 aSlice[0], aSlice[1], aSlice[2] 也就是含有: d,e,f
bSlice = aSlice[0:5] // 对slice的slice可以在cap范围内扩展,此时bSlice包含:d,e,f,g,h
bSlice = aSlice[:] // bSlice包含所有aSlice的元素: d,e,f,g

备注:cap函数只计算左指针到原array最后的值的个数。假设array[3:7],左指针为3,那么cap的容量就是3-10,为7。(左闭右开)

cap指切片的容量或数组的长度。

直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3

image-20240531095359396

slice像一个结构体,这个结构体包含了三个元素:

  1. 一个指针,指向数组中slice指定的开始位置

  2. 长度,即slice的长度(len)

  3. 最大长度,也就是slice开始位置到数组的最后位置的长度(cap)

    实际存储结构图:

img

切片的追加与截取:

  • len 获取slice的长度
  • cap 获取slice的最大容量
  • appendslice里面追加一个或者多个元素,然后返回一个和slice一样类型的slice
  • copy 函数copy从源slicesrc中复制元素到目标dst,并且返回复制的元素的个数
1
2
3
4
slice = array[2:4:7]
//这个的容量就是7-2,即5。这样这个产生的新的slice就没办法访问最后的三个元素。
s = append(s, 100)
//表示将整数 100 追加到切片 s 的末尾
2.5.2 map

map和slice数据结构不同。map的使用方式:引用传递。map是无序的。

1
map[keyType]valueType	// 格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package main
import (
"fmt"
)

func main() {
//第一种声明
var test1 map[string]string
//在使用map前,需要先make,make的作用就是给map分配数据空间
test1 = make(map[string]string, 10)
test1["one"] = "php"
test1["two"] = "golang"
test1["three"] = "java"
fmt.Println(test1) //map[two:golang three:java one:php]


//第二种声明
test2 := make(map[string]string)
test2["one"] = "php"
test2["two"] = "golang"
test2["three"] = "java"
fmt.Println(test2) //map[one:php two:golang three:java]

//第三种声明
test3 := map[string]string{
"one" : "php",
"two" : "golang",
"three" : "java",
}
fmt.Println(test3) //map[one:php two:golang three:java]



language := make(map[string]map[string]string)
language["php"] = make(map[string]string, 2)
language["php"]["id"] = "1"
language["php"]["desc"] = "php是世界上最美的语言"
language["golang"] = make(map[string]string, 2)
language["golang"]["id"] = "2"
language["golang"]["desc"] = "golang抗并发非常good"

fmt.Println(language) //map[php:map[id:1 desc:php是世界上最美的语言] golang:map[id:2 desc:golang抗并发非常good]]


//增删改查
// val, key := language["php"] //查找是否有php这个子元素
// if key {
// fmt.Printf("%v", val)
// } else {
// fmt.Printf("no");
// }

//language["php"]["id"] = "3" //修改了php子元素的id值
//language["php"]["nickname"] = "啪啪啪" //增加php元素里的nickname值
//delete(language, "php") //删除了php子元素
fmt.Println(language)
}

2.5 struct

我们可以声明新的类型,作为其它类型的属性或字段的容器。例如,我们可以创建一个自定义类型person代表一个人的实体。这个实体拥有属性:姓名和年龄。这样的类型我们称之struct

1
2
3
4
type person struct {
name string
age int
}
  • 一个string类型的字段name,用来保存用户名称这个属性
  • 一个int类型的字段age,用来保存用户年龄这个属性
1
2
3
4
var P person  // P现在就是person类型的变量了
P.name = "Astaxie" // 赋值"Astaxie"给P的name属性.
P.age = 25 // 赋值"25"给变量P的age属性
fmt.Printf("The person's name is %s", P.name) // 访问P的name属性.

除了上面这种P的声明使用之外,还有另外几种声明使用方式:

  • 1.按照顺序提供初始化值

    P := person{“Tom”, 25}

  • 2.通过field:value的方式初始化,这样可以任意顺序

    P := person{age:24, name:”Tom”}

  • 3.当然也可以通过new函数分配一个指针,此处P的类型为*person

    P := new(person)

struct的匿名字段

当匿名字段是一个struct的时候,那么这个struct所拥有的全部字段都被隐式地引入了当前定义的这个struct。

能够实现字段的继承。也称为嵌入字段

1
2
3
4
5
6
7
8
9
type Human struct {
name string
age int
weight int
}
type Student struct {
Human // 匿名字段,那么默认Student就包含了Human的所有字段
speciality string
}

问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?

Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。

1
2
3
fmt.Println("Bob's work phone is:", Bob.phone)
// 如果我们要访问Human的phone字段,如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。
fmt.Println("Bob's personal phone is:", Bob.Human.phone)

2.6 面向对象方法

函数的另一种形态,带有接收者的函数,我们称为method

method是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在func后面增加了一个receiver(也就是method所依从的主体)。

语法:

1
func (r ReceiverType) funcName(parameters) (results)
  • 虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
  • method里面可以访问接收者的字段
  • 调用method通过.访问,就像struct里面访问字段一样

指针:Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。

一般解决问题都是通过问题的描述,去写相应的代码实现。

指针作为receiver

一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//下面代码将int用另一种声明代替,之后在声明int型时可以使用myint
type myint int

//定义一个结构体
type Book struct{
title string
auth string
}

//指针传递
func changeBook2(book *Book) {
book.auth = "222"
}
func main() {
var book1 Book //定义一个结构体实体
book1,title = "gogo"
book1.auth = "gopher"

fmt.Printf("%v\n", book1)
}

method继承

Go继承:子类继承父类,添加属性。

method也是可以继承的。如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type Human struct {
name string
age int
phone string
}
type Student struct {
Human //匿名字段
school string
}
type Employee struct {
Human //匿名字段
company string
}
//在human上面定义了一个method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
func main() {
mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}
mark.SayHi()
sam.SayHi()
}

method重写

上面的例子中,如果Employee想要实现自己的SayHi,怎么办?简单,和匿名字段冲突一样的道理,我们可以在Employee上面定义一个method,重写了匿名字段的方法。

1
2
3
4
5
6
7
8
9
//Human定义method
func (h *Human) SayHi() {
fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}
//Employee的method重写Human的method
func (e *Employee) SayHi() {
fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
e.company, e.phone) //Yes you can split into 2 lines here.
}

2.7 interface

多态:一定有父类(有接口);有子类(实现了父类的全部接口);父类类型的变量(指针)指向(引用)子类的具体数据变量

通用万能类型:interface{},空接口。,任意的类型都实现了空interface,也就是包含0个method的interface。

interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。

interface就是一组抽象方法的集合,它必须由其他非interface类型实现,而不能自我实现, Go通过interface实现了duck-typing:即”当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子”。

interface变量存储的类型

我们知道interface的变量里面可以存储任意类型的数值(该类型实现了interface)。那么我们怎么反向知道这个变量里面实际保存了的是哪个类型的对象呢?目前常用的有两种方法:

  • Comma-ok断言

Go语言里面有一个语法,可以直接判断是否是该类型的变量: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。

如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。

  • switch测试

嵌入interface

如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。

2.8 反射

所谓反射就是能检查程序在运行时的状态。

要去反射是一个类型的值(这些值都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数)。这两种获取方式如下:

1
2
t := reflect.TypeOf(i)    //得到类型的元数据,通过t我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过v我们获取存储在里面的值,还可以去改变值

转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值,例如

1
2
tag := t.Elem().Field(0).Tag  //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值

获取反射值能返回相应的类型和数值

1
2
3
4
5
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())

最后,反射的话,那么反射的字段必须是可修改的,

1
2
3
4
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)

动态推测并获取数据的类型(type);

反射包:report

反射解析结构体标签Tag:

结构体标签在JSON中的使用:


3. Gorountine

3.1 介绍

多进程造成:高消耗调度cpu、高内存占用。

协程(co-rounine)介绍:

  • 将线程一分为二:用户线程 + 内核线程(绑定),1:1
  • (进阶)协程调度器,连接多个协程,1:n

Gorountine:

image-20240609104846449

GoProcessor的数量就是Go并行的数量。

特点:

复用线程:

  1. work stealing机制,当有线程(a)的本地队列有等待,而有线程(b)的本地队列无等待时,b会偷取a的一个队列成员。
  2. hand off机制,

利用并行:

抢占:image-20240609110456153

全局G队列: 在work stealing机制中,如果其他本地队列都没有,某空队列会尝试去全局队列偷取。

3.2 创建

只需在函数调⽤语句前添加 go 关键字,就可创建并发执⾏单元。开发⼈员无需了解任何执⾏细节,调度器会自动将其安排到合适的系统线程上执行。

通过return返回是无法拿到函数布尔值bool,所以需要通信通道channel

1
runtime.Goexit()      //退出当前的协程(goroutine)


4. Channel

4.1定义

Channel:管道 ➡ 用来通信 通过它并发核心单元就可以发送或者接收数据进行通讯

1
2
3
4
5
// channel的定义,先创建再使用        操作符: <-
make(chan Type) //等价于make(chan Type, 0)
make(chan Type, capacity)
make(chan int) cs := make(chan string)
cf := make(chan interface{})

capacity:当参数capacity**= 0** 时,channel 是无缓冲阻塞读写的;当capacity > 0 时,channel 有缓冲、是非阻塞的,直到写满 capacity个元素才阻塞写入。(检查通道是否关闭)

1
2
3
4
channel <- value      //发送value到channel     ex.   channel <- 666
<-channel //接收并将其丢弃
x := <-channel //从channel中接收数据,并赋值给x
x, ok := <-channel //功能同上,同时检查通道是否已关闭或者是否为空

通过阻塞,同步读取

channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。

所谓阻塞,也就是如果读取(value := <-ch)它将会被阻塞,直到有数据接收。其次,任何发送(ch<-5)将会被阻塞,直到数据被读出。无缓冲channel是在多个goroutine之间同步很棒的工具。


4.2 缓冲和无缓冲同步问题

无缓冲channel

ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞,直到其他goroutine从channel 中读取一些元素,腾出空间。

当 value = 0 时,channel 是无缓冲阻塞读写的,当value > 0 时,channel 有缓冲、是非阻塞的,直到写满 value 个元素才阻塞写入。

  1. 两个goRoutine都到达通道,但那个都没有开始执行发送或者接收。

  2. 执行发送的goRoutine将手伸进通道,如果没人接收,这个goRoutine就会被锁住。

  3. 执行接收的goRoutine将手伸进通道,如果没人发送,这个goRoutine也会被锁住。

  4. 两者都将手伸进通道,并进行交换后,两个goRoutine才会将手收回,也就是被锁住的goRoutine得到了释放,才可以继续执行后续的流程。

    image-20240609134839028

有缓冲channel(即设定容量的channel)

  1. 发送和接收数据的goRoutine互不影响,均可独立完成发送和接收。即容量满之前没有阻塞限制。
  2. 若盒子满了,则发送数据的goRoutine会阻塞,直到数据被接收取走,才可继续发送数据。

关闭channel特点

1
close()  // 关闭一个channel

l channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;

l 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);

l 关闭channel后,可以继续从channel接收数据;

l 对于nil channel,无论收发都会被阻塞。


4.3 range

可以通过range读取多次c,像操作slice或者map一样操作缓存类型的channel。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
c := make(chan int)

go func() {
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}()

for data := range c {
fmt.Println(data)
}
fmt.Println("Finished")
}

4.4 select

通过select可以监听channel上的数据流动,与switch语句相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作。

当多个channel都准备好的时候,select是随机的选择一个执行的。

1
2
3
4
5
6
7
8
select {
case <- chan1:
// 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1:
// 如果成功向chan2写入数据,则进行该case处理语句
default:
// 如果上面都没有成功,则进入default处理流程,当c阻塞的时候执行这里
}

select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。

超时

有时候会出现goroutine阻塞的情况,那么我们如何避免整个程序进入阻塞的情况呢?我们可以利用select来设置超时。

runtime goroutine

runtime包中有几个处理goroutine的函数:

  • Goexit

    退出当前执行的goroutine,但是defer函数还会继续调用

  • Gosched

    让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。

  • NumCPU

    返回 CPU 核数量

  • NumGoroutine

    返回正在执行和排队的任务总数

  • GOMAXPROCS

    用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

超时强踢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
	for {
select {
case <-isLive:
//当前用户是活跃的,应该重置定时器
//不做任何事情,为了激活select,更新下面的定时器

case <-time.After(time.Second * 10):
//已经超时
//将当前的User强制的关闭
user.SendMsg("你被踢了")
//销毁用的资源
close(user.C)
//关闭连接
conn.Close()
//退出当前Handler
return //runtime.Goexit()
}
}
// islive表示用户有活跃行为
// select监听当前channel管道,假如islive为true,那么进入这个case执行语句,此时没有任何语句,那么for进入下一次select。
// 假如islive为false,那么select不会执行该case,直到阻塞等到channel管道消息,十秒后进入超时条件语句中执行一系列操作。

5. Go Modules

Go语言的依赖解决方案。淘汰了Gopath的使用模式。Go moudles 目前集成在 Go 的工具链中,只要安装了 Go,自然而然也就可以使用 Go moudles 了。

1
2
3
4
go env  //查看环境变量
// GOROOT 源码包文件
// pkg/ 加速编译
// github.com/ github的开源文件

GOPATH的弊端:

  • A. 无版本控制概念. 在执行go get的时候,你无法传达任何的版本信息的期望,也就是说你也无法知道自己当前更新的是哪一个版本,也无法通过指定来拉取自己所期望的具体版本。

  • B.无法同步一致第三方版本号. 在运行 Go 应用程序的时候,你无法保证其它人与你所期望依赖的第三方库是相同的版本,也就是说在项目依赖库的管理上,你无法保证所有人的依赖版本都一致。

  • C.无法指定当前项目引用的第三方版本号. 你没办法处理 v1、v2、v3 等等不同版本的引用问题,因为 GOPATH 模式下的导入路径都是一样的,都是github.com/foo/bar

5.1 go mod命令

命令 作用
go mod init + 新项目名字 生成 go.mod 文件
go mod download 下载 go.mod 文件中指明的所有依赖
go mod tidy 整理现有的依赖
go mod graph 查看现有的依赖结构
go mod edit 编辑 go.mod 文件
go mod vendor 导出项目所有的依赖到vendor目录
go mod verify 校验一个模块是否被篡改过
go mod why 查看为什么需要依赖某模块

(2)go mod环境变量

1
go env

GOPROXY

这个环境变量主要是用于设置 Go 模块代理(Go module proxy),其作用是用于使 Go 在后续拉取模块版本时直接通过镜像站点来快速拉取。

GOPROXY 的默认值是:https://proxy.golang.org,direct

proxy.golang.org国内访问不了,需要设置国内的代理.

1
2
$ go env -w GOPROXY=https://goproxy.cn,direct
// direct 用于指示Go回源到模块版本的源地址去抓取(比如GitHub等)

5.2 初始化项目

image-20240611152554297

改变模块依赖关系:

edit -replace

6. 即时通信系统

image-20240611153644355

6.1 Sever构建

定义一个Server结构体,

1
type Server struct

创建一个Server接口,仅仅是一个接口

1
func NewServer(ip string, port int) *Server

启动服务器的接口,不仅是接口,还是Server结构体的成员方法

1
func (s *Server) Start() 

此时就可以建立链接了

分别建立server.go 和 main.go

1
2
3
4
5
6
7
//main.go
package main

func main() {
server := NewServer("127.0.0.1", 8888)
server.Start()
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
//server.go
package main

import (
"fmt"
"net"
)

// 定义一个Server结构体,用于表示服务器的信息,包括IP地址和端口号
type Server struct {
Ip string
Port int
}

// 创建一个Server接口,N大写代表函数是对外开放的
func NewServer(ip string, port int) *Server { // 返回一个指针Server
server := &Server{ // new一个对象
Ip: ip,
Port: port,
}
return server
}

func (s *Server) Handler(conn net.Conn) {
//当前链接的业务
fmt.Println("建立成功咯")
}

// 启动服务器的接口
func (s *Server) Start() {
// socket linsten
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Ip, s.Port))
if err != nil {
fmt.Println("net.listen error:", err)
}
// closen listen socket
defer listener.Close() //在成功之后,防止忘掉close

for {
// accept ,成功后返回一个链接,和当前客户当建立成功
conn, err := listener.Accept()
if err != nil {
fmt.Println("listener.accept error:", err)
continue
}
// do handler 成功后,启动一个goroutine处理conn,处理业务
go s.Handler(conn)
}
}

写好代码后,在终端中

1
2
$ go build -0 server.exe  //编译
$ ./server //运行server

新建终端,链接server

1
2
$ telnet 127.0.0.1 8888 //windows
$ nc 127.0.0.1 8888 //linux

显示:

1
建立成功咯

6.2 用户上线功能

image-20240612085431144

user1中,建立有关user的结构体User,并定义变量user。

1
type User struct

创建一个用户的API

1
func NewUser(conn net.Conn) *User{}

同时在API中,需要开启一个goroutine去监听channel,一旦有消息,就将消息发送给客户端。

1
go user.ListenMessage()

最后再写一个goroutine监听的方法。

server中,在Server结构体中加入OnlineMap(在线用户列表),再加入消息广播的Message,

在server接口中加入OnlineMap和Message(channnel)。在handler中用户上线后将用户加入OnlineMap中,将map上锁,将上线的user.Name传给Map表中的user,

创建Handler接受处理http请求,再广播当前用户上线的消息(需要一个广播消息的方法func,在此方法中将Message(“已上线”)发送给客户),handler需要阻塞,所以加入一个select{}。

需要一个goroutine方法不断监听广播消息的channel,有消息就发送给user,

然后在start()接口中加入启动go监听的代码。

gorountine:负责监听、发送

channel:负责通信

user.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import "net"

//建立User结构体
type User struct {
Name string //用户名
Addr string //地址
C chan string //用于发送消息的channel(绑定)
conn net.Conn //连接
}

//创建用户API
func NewUser(conn net.Conn) *User {
userAddr := conn.RemoteAddr().String() //获取客户端连接地址

user := &User{
Name: userAddr,
Addr: userAddr,
C: make(chan string),
conn: conn,
}
// 启动监听当前user, channel 的消息的gorountine
go user.ListenMessage()

return user
}

//监听当前user channel的方法,一旦有消息,就直接发送给对端客户端
func (u *User) ListenMessage() {
for {
msg := <-u.C //监听channel的读取

u.conn.Write([]byte(msg + "\n")) //将消息转为二进制字节发送给客户端
}
}

server.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
package main

import (
"fmt"
"net"
"sync"
)

// 定义一个Server结构体,用于表示服务器的信息,包括IP地址和端口号
type Server struct {
Ip string
Port int

// 在线用户列表
OnlineMap map[string]*User
maplock sync.RWMutex

// 消息广播的channel
Message chan string
}

// 创建一个Server接口,N大写代表函数是对外开放的
func NewServer(ip string, port int) *Server { // 返回一个指针Server
server := &Server{ // new一个对象
Ip: ip,
Port: port,
OnlineMap: make(map[string]*User),
Message: make(chan string),
}
return server
}

// 监听Message广播消息channel的goroutine,一旦有消息就发送给全部在线的User
func (s *Server) ListenMessage() {
for {
msg := <-s.Message

//发送消息
s.maplock.Lock()
for _, cli := range s.OnlineMap {
cli.C <- msg
}
s.maplock.Unlock()
}
}

// 广播消息的方法
func (s *Server) BroadCast(user *User, msg string) {
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

s.Message <- sendMsg //
}

func (s *Server) Handler(conn net.Conn) {
//当前链接的业务
//fmt.Println("建立成功咯")
user := NewUser(conn)
//用户上线后,将用户加入OnlineMap广播中
s.maplock.Lock()
s.OnlineMap[user.Name] = user // 加入在线用户列表
s.maplock.Unlock()

//广播当前用户上线消息
s.BroadCast(user, "已上线")
//当前handler阻塞
select {}
}

// 启动服务器的接口
func (s *Server) Start() {
// socket linsten
listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", s.Ip, s.Port))
if err != nil {
fmt.Println("net.listen error:", err)
return
}
// closen listen socket
defer listener.Close() //防止遗忘closen

// 启动监听Message的goroutine
go s.ListenMessage()

for {
// accept
conn, err := listener.Accept() //返回一个链接conn
if err != nil {
fmt.Println("listener.accept error:", err)
}
// do handler
go s.Handler(conn) // 启动一个goroutine处理conn
}

}

编写完成后,控制台输入

1
$ go build -o  server.exe main.go server.go user.go

成功编译文件生成server.exe文件,再输入./server运行server

1
2
3
$ ./server
// 或者
$ server

新建两个终端,先输入chcp 65001,修改编码为utf-8

1
$ chcp 65001

再输入地址连接server

1
2
3
$ telnet 127.0.0.1 8888             //telnet + 本机号 + 端口号
// 发送内容,输入"CTRL ]"",在输入“send + 内容”,例如
send nihao

image-20240612100424913

运行成功

遇到的问题:

1
sendMsg := "[" + user.Addr + "]" + user.Name + ":" + msg

在之前我将上述代码中user中的u写成了大写,导致运行出了些问题,在之后我必须严格按照之前定义的变量来使用

1
2
userAddr := conn.RemoteAddr().String() //获取客户端连接地址
user := &User

上述代码从User结构体中定义了一个user结构体变量。

6.3 用户消息广播

6.4 用户业务封装

6.4 在线用户查询

6.5 修改用户名

6.6 超时强踢功能

6.7 私聊功能

6.8 在线连接

6.9 命令行解析

6.10 菜单显示

6.11 更新用户名

6.12 公聊模式

6.13 私聊模式


二、标准库

1.1 sync

sync.Map 是适合于读多写少的场景。

sync.Map 的各个字段。

  • mu:互斥锁,用于保障 read 和 dirty 的并发安全;
  • read:可直接并发读的 map 数据,未存储全量数据;
  • dirty:并发不安全的 map 数据,存储全量数据或为 nil (如果为 nil 时 read 存储全量数据);
  • misses:未命中 read 的次数,用于记录是否需要将 dirty 转换为 read。

适合于写多的场景

对 map 进行分片,通过多个 map 降低写操作加锁阻塞的概率。

传入分片的数量和分片函数,然后我们将根据分片数量 N 初始化 N 个 sync.RWMutex 和 N 个 map。每个分片之间相互独立,当需要读或写操作时,先计算出对应的 key 在哪个分片上,然后再操作该分片。


1.2 os

stderr介绍:

stderr 是标准错误流(standard error stream),通常用于输出程序的错误信息。与 stdout(标准输出流)不同,stderr 通常用于显示程序运行时的错误消息、警告和其他诊断信息。

标准输出流 (stdout) 与标准错误流 (stderr)

  • **stdout**:标准输出流,通常用于输出程序的正常输出。默认情况下,这些输出会显示在终端上,但也可以被重定向到文件或其他地方。
  • **stderr**:标准错误流,通常用于输出程序的错误信息和调试信息。默认情况下,这些输出也会显示在终端上,但可以通过不同的方式处理,例如重定向到单独的日志文件。

在 Go 中使用 stderr

1
2
3
4
func main() {
// 向 stderr 写入错误信息
fmt.Fprintln(os.Stderr, "This is an error message")
}

抛出 Panic 并向 stderr 输出信息

在 Go 中抛出一个 panic 时,默认情况下,Go 运行时会捕获这个 panic 并将相关信息输出到 stderr

1
2
3
4
func main() {
// 抛出 panic
panic("Something went wrong!")
}

自定义 Panic 处理并输出到 stderr

自定义 panic 的处理逻辑,并且明确地将信息写入 stderr,可以使用 recover 函数来捕获 panic,并在 defer 语句中处理它。

1
2
3
4
5
6
7
8
9
10
11
func main() {
defer func() {
if r := recover(); r != nil {
// 将 panic 信息写入 stderr
fmt.Fprintln(os.Stderr, "Recovered from panic:", r)
}
}()

// 抛出 panic
panic("Something went wrong!")
}

Go代码规范及意义

1. 错误数据处理

1
2
3
if err != nil {
fmt,Println("配置文件读取错误")
}

2.命名规范

2.1 Go程序设计的一些规则

Go之所以会那么简洁,是因为它有一些默认的行为:

  • 大写字母开头的变量是可导出的,也就是其它包可以读取的,是公有变量;小写字母开头的就是不可导出的,是私有变量。
  • 大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。

2.2 Go是不是面向编程?

  • Go 语言中,没有对象(但是开发时通常叫做对象),没有类,也没有继承,也不能直接使用多态。
  • 它通过组合匿名字段的手段,来达到类似继承的效果。
  • 它只能通过接口,来实现的多态,使一个类可以有不同的实现。
  • 将一类事物,抽象成struct的过程,这才是封装的主要部分
  • Go语言 sturct 方法的写法,和其他面向对象语言中 this 的本质很像。

x.VsCode使用Go

a.设置goproxy

GoProxy是一个由Go语言编写的代理服务器,它具有多种功能和特点。

首先,GoProxy是一个高效、安全的HTTP/HTTPS代理服务器,旨在帮助开发者在进行网络请求时实现数据的中继和过滤,从而增强安全性并提升开发效率。它的核心设计基于Go语言的并发模型(goroutines和channels),这使得它能够充分利用多核CPU,提供出色的性能表现。

1
go env -w GOPROXY=https://goproxy.cn,direct  //cmd控制台修改

go mod: 管理一切

1
go mod init +项目名   //生成.mod程序  初始化项目
1
go build //生成exe文件

改用cmd控制台