声明:本文为b站大佬刘丹冰Aceld8小时转职Golang工程师视频学习笔记,如有侵权,联系我速删

大佬的传送门:https://www.bilibili.com/video/BV1gf4y1r79E/?spm_id_from=333.999.0.0&vd_source=95a9bb0b0759f8d391968411c1a6f007

在b站发现了大佬的宝藏视频,在此记录下学习过程。我在这里着重学习记录的是go语言的特性,也就是go与其他后端语言有区别的地方。并且在文章的末尾附上我使用go语言开发的即时通信系统的github地址。

一、目录

1.初识go语言

2.变量与常量

3.函数的返回值与defer

4.golang中的数组

5.golang中的map

6.golang面向对象基础

7.golang中的反射

8.goroutine和channel

二、初识go语言

1
2
3
4
5
6
7
8
9
10
11
package main //程序的包名

import ( //程序导包
"fmt"
"time"
)

func main() { //函数声明行末必须是左花括号
fmt.Println("Hello,go!!!")
time.Sleep(2 * time.Second)
}

运行结果:

image-20230109105022987

知识点:

1.程序的第一行是包名,使用package关键字

2.使用import关键字进行导包

3.go语言中函数声明行末必须是左花括号

三、变量与常量

1.声明变量的四种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main

import "fmt"

func main() {
//1.声明一个变量,不赋初值,默认为0
var a int
fmt.Println("a =", a)
//2.为变量赋初值
var b string = "字符串"
fmt.Println("b =", b)
//3.省去数据类型,自动推断
var c = 100.00
fmt.Printf("c is %T\n", c)
//4.省去var关键字
//注意:冒等不可用来声明全局变量,即只能是在函数体内使用
d := 60
fmt.Printf("the type of c is %T\n", d)
}

运行结果:

image-20230109110102171

知识点:

(1) 我们通常使用fmt包中的Println函数来输出一行内容,相当于python中的print函数,其中的逗号表示使用空格连接,会自动在末尾加上换行符。

(2) fmt包中的Printf函数为格式化输出,不会自动加换行符,需要手动添加。其中的格式化参数:

  • %T 打印变量的类型.
  • %v 以默认的方式打印变量的值.例如在结构体中{jack {12345 6789}}
  • %+v 带字段名称.例如在结构体中{name:jack phone:{mobile:12345 office:6789}
  • %t 打印true或false
  • ……

(3) 我们通常在函数体内使用冒等(:=)的写法来初始化变量,并给它赋值

2.声明多个变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
var m, n int = 100, 200
var j, k = 300, "abc"
c, v := 1.5, true
fmt.Println(m, n, j, k, c, v)

var (
z bool = true
x int = 1
)
fmt.Println(z, x)
}

运行结果:

image-20230109111702647

3.const与iota

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
package main

import "fmt"

//const定义枚举
const(
//可以在const使用关键字iota,初值为0,每行+1
MONDAY = iota+1 //iota=0 ,iota+1=1
TUESDAY //iota=1 ,iota+1=2
WEDNESDAY //iota=2 ,iota+1=3
)

const(
a,b = iota+1,iota+2 //iota=0
c,d //iota=1
e,f //iota=2

g,h = iota*2 , iota*3 //iota=3
m,n
)
func main() {
//常量(只读属性)
const length int = 10

fmt.Println(MONDAY,TUESDAY,WEDNESDAY)
fmt.Println(a,b,c,d,e,f,g,h,m,n)
}

运行结果:

image-20230109112016337

知识点:

(1) 常量的定义格式和变量的声明语法类似

(2) iota是常量计数器,在第一行为0,之后每新增一行常量声明将使iota计数一次

四、函数的返回值与defer

1.函数多返回值的几种写法

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
package main

import "fmt"

//只有一个返回值
func foo1(a string, b string) int {
c := 11111
fmt.Println("进入了函数1,并输入两个参数:", a, b)
return c
}

//有多个返回值,匿名的
func foo2(a string, b string) (int, string) {
c := 2222222
d := "222222"
fmt.Println("进入了函数2,并输入两个参数:", a, b)
return c, d
}

//返回多个值,有形参名称的
func foo3(a string, b string) (r1 int, r2 string) {
fmt.Println("进入了函数3,并输入两个参数:", a, b)

//给有名称的返回值变量赋值
r1 = 333
r2 = "333333333"

return
}

//返回多个值,有形参名称的,且形参类型相同,可省略只写一个
func foo4(a string, b string) (r1, r2 int) {
fmt.Println("进入了函数4,并输入两个参数:", a, b)

//给有名称的返回值变量赋值
r1 = 444
r2 = 444444

return
}

func main() {
fmt.Println(foo1("1", "1"))

ret1, ret2 := foo2("2", "2")
fmt.Println(ret1, ret2)

ret3, ret4 := foo3("3", "3")
fmt.Println(ret3, ret4)

ret5, ret6 := foo4("4", "4")
fmt.Println(ret5, ret6)

}

运行结果:

image-20230109153955212

知识点:

(1) 函数声明语法: func 函数名(形参 形参数据类型,……)(返回值参数 返回值类型,…){

(2) 大多数 a int,b int,c int这种形式都可以简写为a, b, c int

(3) 函数名首字母小写表示只能在当前文件内调用,像我们常用的fmt.Println()首字母大写,可以在其他文件内调用

2.defer语句调用流程

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
package main

import "fmt"

//函数名首字母小写表示只能在当前文件内调用
//在函数return之后才会进行defer
func deferAndReturn() int {
defer fmt.Println("func: 调用了defer")
return returnFunc()
}
func returnFunc() int {
fmt.Println("func: 调用了return")
return 1
}

func main() {
//在当前函数结束之前调用,按入栈顺序执行
defer fmt.Println("main: main end1")
defer fmt.Println("main: main end2")

fmt.Println("main: main go 1")
fmt.Println("main: main go 2")

deferAndReturn()
}

运行结果:

image-20230109154930773

知识点:

(1) 使用defer后会将当前语句入栈,在当前函数结束之前依次出栈

(2) 注意defer是在return之后执行的

五、golang中的数组

1.静态数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import "fmt"

func printArray(myArray [10]int){
for index,value := range myArray{
fmt.Println(index,value)
}
}

func main() {
//固定长度数组
var myarray1 [10]int
myarray2 := [10]int{1,2,3,4}

for i:=0; i<len(myarray1); i++ {
fmt.Println(myarray1[i])
}

printArray(myarray2)
}

运行结果:

image-20230109161755282

知识点:

(1) 声明数组后,其中元素默认值为0

(2) 函数的形参和传递实参的长度一定要对应

2.动态数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

func printArray(myArray []int) {
//引用传递,不是值传递
//_表示匿名的变量
for _, value := range myArray {
fmt.Println(value)
}
}
func main() {
//动态数组
//声明并初始化,默认值为1,2,3 长度len是3
slice1 := []int{1, 2, 3}
printArray(slice1)
fmt.Printf("len=%d,slice=%v\n", len(slice1), slice1)

//声明一个动态数组,但是不给他分配空间
var slice2 []int
//使用make开辟3个空间,默认值为0
slice2 = make([]int, 3)

}

运行结果:

image-20230109162247440

3.动态数组的容量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import "fmt"

func main() {
var numbers = make([]int, 3, 5)
//长度和容量
fmt.Printf("len=%d,cap=%d,slice=%v\n", len(numbers), cap(numbers), numbers)
//向slice中追加元素1
numbers = append(numbers, 1)
fmt.Printf("len=%d,cap=%d,slice=%v\n", len(numbers), cap(numbers), numbers)
//再向slice追加两个元素,超过cap
//他会自动扩充一个原始cap的大小,我们可以将看到cap变成了10(5+5)
numbers = append(numbers, 2)
numbers = append(numbers, 3)
fmt.Printf("len=%d,cap=%d,slice=%v\n", len(numbers), cap(numbers), numbers)

}

运行结果:

image-20230109162344449

知识点:

(1) 使用make为数组开辟空间时,可以指定数组的cap,即数组的最大容量

(2) 当向slice添加的元素超过cap时,会自动扩充一个原始cap的大小

六、golang中的map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func main() {
//声明一个map
var myMap1 map[string]string

//给map分配空间
myMap1 = make(map[string]string, 10)
myMap1["one"] = "java"
myMap1["two"] = "c++"

fmt.Println(myMap1)

//另一种声明方式
myMap2 := map[string]string{
"a": "aa",
"b": "bb", //末尾记得加逗号
}
fmt.Println(myMap2)
}

运行结果:

image-20230109162934736

七、golang面向对象基础

1.struct的使用

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
package main

import "fmt"

//type关键字 声明一种新的数据类型 ,int的一个别名
type myint int

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

func printBook(book Book) {
//值传递,传递一个book的副本,不改变值
fmt.Printf("%v\n", book)
}

func changeBook(book *Book) {
//指针传递
book.title = "已经被改变了"
}

func main() {
var book1 Book
book1.title = "Golang"
book1.auth = "lisi"

// %v表示可以打印任何类型的变量
fmt.Printf("%v\n", book1)

//要传地址
changeBook(&book1)
fmt.Printf("%v\n", book1)
}

运行结果:

image-20230109163244795

2.创建一个对象

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
package main

import "fmt"

type Hero struct {
Name string
Ad int
Level int
}

//名字首字母大写=public 首字母小写=private
func (this *Hero) Show() {
fmt.Println("Name =", this.Name)
fmt.Println("Ad =", this.Ad)
fmt.Println("Level =", this.Level)
}
func (this *Hero) GetName() string {
return this.Name
}

//注意set中需要改变值,需要传递指针
func (this *Hero) SetName(newName string) {
this.Name = newName
}
func main() {
//创建对象
hero := Hero{Name: "zhangsan", Ad: 100, Level: 2}
//
fmt.Println(hero.GetName())
hero.SetName("已经改变")
hero.Show()
}

运行结果:

image-20230109163430306

知识点:

(1) 注意成员方法的写法,要在func和函数名之间加上**(变量名 *类名)**,变量名通常用this

3.类的继承

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
package main

import "fmt"

type Human struct {
name string
sex string
}

func (this *Human) Eat() {
fmt.Println("开饭了")
}

func (this *Human) Walk() {
fmt.Println("走起来了")
}

type SuperMan struct {
Human //SuperMan继承了Human类的方法

level int
}

//重新定义父类的方法Eat()
func (this *SuperMan) Eat() {
fmt.Println("超人开饭了")
}

//子类的新方法
func (this *SuperMan) Fly() {
fmt.Println("超人飞起来了")
}
func main() {
human := Human{"zhangsan", "female"}
human.Eat()

//定义一个子类对象
// s := SuperMan{Human{"lisi","male"},99}
var s SuperMan
s.name = "lisi"
s.sex = "male"
s.level = 99
s.Walk()
s.Eat()
s.Fly()
}

运行结果:

image-20230109163924166

4.创建一个接口

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
package main

import "fmt"

//本质是一个指针
type AnimalIF interface{
Sleep()
GetColor() string
GetType() string
}

//具体的类
type Cat struct{
color string
}
//实现接口的所有方法就是实现它
func (this *Cat) Sleep(){
fmt.Println("猫在睡觉")
}
func (this *Cat) GetColor() string {
return this.color
}
func (this *Cat) GetType() string {
return "cat"
}

type Dog struct{
color string
}
func (this *Dog) Sleep(){
fmt.Println("狗在睡觉")
}
func (this *Dog) GetColor() string {
return this.color
}
func (this *Dog) GetType() string {
return "dog"
}

func showAnimal(animal AnimalIF){
animal.Sleep()//多态
fmt.Println("color = ",animal.GetColor())
fmt.Println("type = ",animal.GetType())
}
func main() {
/**var animal AnimalIF//接口的数据类型 父类指针
animal = &Cat{"Green"}
animal.Sleep()

animal = &Dog{"Yellow"}
animal.Sleep()**/
cat := Cat{"Green"}
dog := Dog{"Yellow"}
showAnimal(&cat)
showAnimal(&dog)
}

运行结果:

image-20230109164340679

知识点:

(1) 接口使用interface关键字

(2) 当一个类实现了接口的所有方法后,就相当于是实现了这个接口

5.interface{}万能数据类型

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
package main

import "fmt"

//interface{}是万能数据类型
func myFunc(arg interface{}) {
fmt.Println(arg)

//interface{}如何区分此时引用的底层数据类型是什么呢
//给interface{}提供“类型断言”机制
value, ok := arg.(string)
if !ok {
fmt.Println("arg is not string")
} else {
fmt.Println("arg is string,value =", value)
fmt.Printf("arg type is %T\n", value)
}
}

type Book struct {
auth string
}

func main() {
book := Book{"Golang"}
myFunc(book)
myFunc("1234")
}

运行结果:

image-20230109164657848

知识点:

(1) 感觉interface{}类似于java中的Object

八、golang中的反射

1.变量的内置pair结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import "fmt"

/**
变量
| |
type value -> pair
| |
static type concrete type
**/

func main() {
var a string
//pair<static type:string,value:"abcd">
a = "abcd"

//pair<type:string,value:"abcd">
var allType interface{}
allType = a

str, _ := allType.(string)
fmt.Println(str)
}

运行结果:

image-20230109165116718

知识点:

(1) 每个变量都内置一个键值对(type, value)

(2) 变量不管给谁赋值,变量的pair都不会改变(pair连续传递

一些例子:

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
package main

import (
"fmt"
"io"
"os"
)

func main() {
//tmp: pair<type:*os.File, value:"D:\Temp\tmp.txt"文件描述符>
tmp, err := os.OpenFile("D:\\Temp\\tmp.txt", os.O_RDWR, 0)

if err != nil {
fmt.Println("error", err)
return
}

//r: pair<type: , value: >
var r io.Reader
//r: pair<type:*os.File, value:"D:\Temp\tmp.txt"文件描述符>
r = tmp

//w: pair<type: , value: >
var w io.Writer
//w: pair<type:*os.File, value:"D:\Temp\tmp.txt"文件描述符>
w = r.(io.Writer)

w.Write([]byte("Hello world!!\n"))

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

import "fmt"

type Reader interface {
ReadBook()
}
type Writer interface {
WriteBook()
}
type Book struct{}

func (this *Book) ReadBook() {
fmt.Println("Read...")
}
func (this *Book) WriteBook() {
fmt.Println("Write...")
}
func main() {
//b:pair<type:Book,value:book{}的地址>
b := &Book{}

//r:pair<type: , value: >
var r Reader
//r: pair<type:Book,value:book{}的地址>
r = b

r.ReadBook()

var w Writer
//w: pair<type:Book,value:book{}的地址>
w = r.(Writer)
w.WriteBook()
}

image-20230109165629812

2.使用reflect获取变量的value和type

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
"reflect"
)

func reflectNum(arg interface{}) {
fmt.Println("type: ", reflect.TypeOf(arg))
fmt.Println("value: ", reflect.ValueOf(arg))
}

func main() {
var num float64 = 1.2345
reflectNum(num)
}

运行结果:

image-20230109165849877

3.使用reflect获取类对象的字段和方法

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
package main

import (
"fmt"
"reflect"
)

type User struct {
Id int
Name string
Age int
}

func (this User) Call() {
fmt.Println("user is called")
fmt.Printf("%v\n", this)
}
func main() {
user := User{1, "zhangsan", 8}
DoFileAndMethod(user)
}
func DoFileAndMethod(input interface{}) {
//获取input的type
inputType := reflect.TypeOf(input)
fmt.Println("inputType: ", inputType.Name())
//获取input的value
inputValue := reflect.ValueOf(input)
fmt.Println("inputValue: ", inputValue)

//通过type获取里面的字段
//1. 获取interface的reflect.Type ,通过Type得到NumField,进行遍历
//2.得到每个field, 数据类型
//3. 通过field有一个Interface()方法得到对应的value
for i := 0; i < inputType.NumField(); i++ {
field := inputType.Field(i)
value := inputValue.Field(i).Interface()

fmt.Printf("%s: %v = %v\n", field.Name, field.Type, value)
}

//通过type获取里面的方法,调用
for i := 0; i < inputType.NumMethod(); i++ {
m := inputType.Method(i)
fmt.Printf("%s: %v\n", m.Name, m.Type)
}
}

运行结果:

image-20230109170055450

九、goroutine和channel

1.创建一个goroutine

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
package main

import (
"fmt"
"time"
)

func newTask() {
i := 0
for {
i++
fmt.Printf("new goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}

// 主goroutine
func main() {
//创建一个go程(goroutine),去执行newTask
go newTask()

i := 0

for {
i++
fmt.Printf("main goroutine : i = %d\n", i)
time.Sleep(1 * time.Second)
}
}

运行结果:

image-20230109203711121

知识点:

(1) 使用go关键字可以开启一个go程

(2) 可以使用go加匿名函数来创建一个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
package main

import (
"fmt"
"time"
// "runtime"
)


func main() {
/**
//用go创建承载一个形参为空,返回值为空的一个函数
go func(){
defer fmt.Println("A.defer")
func() {
defer fmt.Println("B.defer")
//退出当前goruntine
// runtime.Goexit()
fmt.Println("B")
}()
fmt.Println("B")
}()
**/

go func(a int, b int) bool{
fmt.Println("a =",a,"b = ",b)
return true
}(10,20)

//死循环,防止主go程死掉,导致子go程死掉
for{
time.Sleep(1 * time.Second)
}
}

注意:使用go+func创建go程后要在最后使用括号调用函数

2.创建一个channel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import "fmt"

// 1.当main协程先到达num := <- c,会发生阻塞,等待子协程运行到c <- 666,然后唤醒main协程
// 2.当子协程先到达c <- 666,同样会发生阻塞,等待主协程读取后再唤醒子协程
func main() {
//定义一个channel
c := make(chan int)

go func() {
defer fmt.Println("go1结束")

fmt.Println("go1正在运行....")

c <- 666 //将666发送给c
}()

num := <-c //从c中接收数据,赋值给num

fmt.Println("num = ", num)
fmt.Println("main gorountine结束")
}

运行结果:

image-20230109204659473

知识点:

(1) 使用**make(chan int)**声明并初始化一个int类型的channel

(2) 在channel中读数据: num := <-c ,写数据:c <- 666

3.带缓冲的channel

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
package main

import (
"fmt"
"time"
)

func main() {
//定义一个带缓冲的channel
c := make(chan int, 3)
//len是当前缓存有多少元素,cap是缓存的容量
fmt.Println("len(c) = ", len(c), "cap(c)", cap(c))

go func() {
defer fmt.Println("子go程结束")

for i := 0; i < 5; i++ {
c <- i
fmt.Println("子go程正在运行 len(c) = ", len(c), "cap(c)", cap(c))
}
}()

time.Sleep(2 * time.Second)

for i := 0; i < 3; i++ {
num := <-c
fmt.Println("num =", num)
}

fmt.Println("main结束")
}

运行结果:

image-20230109210026928

知识点:

(1) 当子go程向channel写了3个元素之后,由于主go程的Sleep,并没有人读channel,导致子go程阻塞。随后,主go程睡醒,读取三个元素然后结束,就导致了子go程的defer语句没有输出

(2) 当channel满了之后,再想写入就会阻塞当前go程

4.使用close关闭channel

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
package main

import (
"fmt"
)

func main() {

c := make(chan int)

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

//close可以关闭一个channel
close(c)
}()

for {
//当ok为false时,表示channel已经关闭且为空;否则返回true
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}

fmt.Println("main结束")
}

运行结果:

image-20230109210719855

知识点:

(1) channel关闭就不可以写了,但是可以接着读

5.使用range来不断迭代操作channel

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
package main

import (
"fmt"
)

func main() {

c := make(chan int)

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

//close可以关闭一个channel
close(c)
}()

// for{
// //当ok为false时,表示channel已经关闭且为空;否则返回true
// if data,ok := <-c; ok{
// fmt.Println(data)
// }else{
// break
// }
// }

//可以使用range来不断迭代操作channel
for data := range c {
fmt.Println(data)
}

fmt.Println("main结束")
}

image-20230109210928897

6.使用select

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
package main

import (
"fmt"
)

func fabonacii(c, quit chan int) {
x, y := 1, 1

for {
select {
case c <- x:
//如果c可以写,就会进这个case
t := y
y, x = x+y, t
case <-quit:
fmt.Println("quit")
return
}
}
}

func main() {

c := make(chan int)
quit := make(chan int)

go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}

quit <- 0
}()

fabonacii(c, quit)
}

运行结果:

image-20230109211020027

十、即时通信系统

跟着老师的课程,使用go语言写了一个即时通信系统的小例子,放在了我的github上

项目地址:https://github.com/feiweiliang/-golang-

实现的功能:

  • 更改用户名
  • 公聊模式
  • 私聊模式
  • 查看当前在线用户
  • 超时强踢

项目截图:

image-20230109213153122

未来打算学完gin之后,改成web界面版的即时通信系统