Navigation
阅读进度0%
No headings found.

Go 核心基础:包管理、数据类型与常用结构

December 19, 2024 (1y ago)

Golang
Package
DataTypes

本文档主要是回顾和学习 Golang ,课程来自蔡超(注意本讲座内容比较分散,你需要集中的进行归纳和总结额)

简单的介绍

go只有25个关紧字,go是编译的强类型语言,支持自动GC 和指针访问,生产力很强大,Go只有 复合 FFP,OPP是不纯在的但是我们可以使用FP的模式来模拟它,另外区块链开发语言也是golang

install go的话非常的简单,我们只需要去 https://www.runoob.com/go/go-environment.html, 对于Mac用户我们只需要brew install go 就好了。

1.8 版本之火必须设置环境变量 ( 但是谁tm的现在还在使用go 1.8?大多数都是11往上了 )

IDE的话,我们使用Vscode 使用的时候需要注意,只能一次打开一个workspce 要不然会有问题

从一个最简单程序来看go

pkg是什么鬼

项目中的包packge是什么的鬼呢?类似前端的module化 ,举例下面的项目,中的pkg文件夹📁。项目中的pkg就是这个程序所需要用到的所有的pkg了,我们拿里面的一个最简单的东西来举个例子🌰 看看pkg到底如何使用,比如 file 这个pkg,它的作用时 处理文件

代码我们放一小段出来

package file
 
import (
	"fmt"
	"io/ioutil"
	"mime/multipart"
	"os"
	"path"
)
 
// 在这里我们用到了 mime/multipart 包(先不用管这个包),它主要实现了 MIME 的 multipart 解析
// 获取文件大小 字节
func GetSize(f multipart.File) (int, error) {
	content, err := ioutil.ReadAll(f)
 
	return len(content), err
}
 
// 获取文件后缀
func GetExt(fileName string) string {
	return path.Ext(fileName)
}
 
// 看看文件是否存在
func CheckNotExist(src string) bool {
	_, err := os.Stat(src)
 
	return os.IsNotExist(err)
}
 
// 看看有没有权限
func CheckPermission(src string) bool {
	// 具体实现省略
}
 
// 如果没有就创建文件夹
func IsNotExistMkDir(src string) error {
	// 具体实现省略
}
 
// 创建文件夹
func MkDir(src string) error {
	// 具体实现省略
}
 
// 打开文件
func Open(name string, flag int, perm os.FileMode) (*os.File, error) {
// 具体实现省略
}
 
// MustOpen maximize trying to open the file
func MustOpen(fileName, filePath string) (*os.File, error) {
// 具体实现省略
}
 

然后我们看看在项目中是如何使用的。我们先看这个packge的名字叫做file 和文件夹保存一致(这是一种规范)

在pkg upload 下使用了

package upload
 
import (
	"fmt"
	"log"
	"mime/multipart"
	"os"
	"path"
	"strings"
 
	"github.com/BM-laoli/go-gin-example/pkg/file"  // 重点是这个 
	"github.com/BM-laoli/go-gin-example/pkg/logging"
	"github.com/BM-laoli/go-gin-example/pkg/setting"
	util "github.com/BM-laoli/go-gin-example/utils"
)
// 这里面有些 github.com/BM-laoli/go-gin-example/pkg/file 先不用管,这个东西和你项目是相关的,后面会讲,目前你可以理解这个就是你本地的那个pkg file

go中的node_modules

我们前面有介绍在go中如何引其他包,但是我们貌似忘记了非常重要的一点,就说go中的包管理到底是如何做的,接下来 引用知乎高赞 https://zhuanlan.zhihu.com/p/269719543,来给大家理清楚go中包管理问题,我个人非常喜欢使用表格来总结知识点,这非常的高效且方便记忆

  1. 我们看看如何初始化一个包 (类比于 npm init)
mkdir yourfiles
cd yourfiles
# 建议name 和你的文件夹保存一致
go mod init yourname 
 
 
  1. 如何在包里互相的引用(就像import xxx前端一样)

有了包之后,我们来组织一下我们的项目结构

# model
package model
 
import "fmt"
 
func GetModalName() {
	fmt.Println("GetModalName")
}
 
# utils
package utils
 
import "fmt"
 
func GetUtilsName() {
	fmt.Println("GetUtilsName")
}
package test_main
 
import "testing"
 
func TestXXX(t *testing.T) {
	t.Log("Testing")
}
 
# test
package test_main
 
import "testing"
 
func TestXXX(t *testing.T) {
	t.Log("Testing")
}
 
# main
package main
 
import (
	"c2/modl/pkg/model"
	"c2/modl/pkg/utils"
	"fmt"
)
 
func main() {
	fmt.Println("222")
	model.GetModalName()
	utils.GetUtilsName()
 
}
 
  1. 看看如何测试 (go中有自带的单元测试)

如果你 使用vscode 那么很方便,点一就好了,如果没有也不要慌 使用 shell go cli 去执行

# 注意啊 一定要进到文件夹里面去
cd ./test/
go test 
 
  1. 如何install 互联网上的包 比如rsc.io

rsc.io/sampler 是个什么东西?这里有它的文档 https://pkg.go.dev/rsc.io/quote#section-readme

go get rsc.io/sampler

使用它也是非常的简单

# main
import (
	"c2/modl/pkg/model"
	"c2/modl/pkg/utils"
	"fmt"
    "rsc.io/quote"
)
 
func main() {
	fmt.Println("222")
	model.GetModalName()
	utils.GetUtilsName()
    Hello()
}
 
func Hello() string {
    return quote.Hello()
}
 
 
 
  1. 那么我有没有它的npm呢?当然了 https://pkg.go.dev/ 这个就说go 的npm 哈哈哈

总结一下最后的流程

画板

go的运行方式

go的运行 有两种方式,直接run / 编译生成二进制(go会自动静态连接🔗)一个独立的二进制文件,通过容器部署非常的方便

# 直接run 一般本地开发这样用
go run main.go
 
# 一般build 部署的时候使用二进制
go build main.go

注意事项

1.关于入口文件 必须上main包:package main

2.关于入口文件 必须上main方法:func main()
3.关于入口文件 不一定说main.go

这些注意事项,通过上面的例子你应该也了解,这里就不多说了。比如下面结构的代码有main方法,他们都可以独立运行 go run hellw.go / go run main.go

退出返回值和 控制台参数的接受

Go 中的mian 不支持返回值,如果要返回程序退出时的状态,需要通过下面的语法 os.Exit() / os.Exit(-1)

package main
 
import (
	"fmt"
	"os"
)
 
func main() {
	fmt.Println("Hello word")
	// os.Exit() 两种都可以区别什么的后面在说
	os.Exit(-1)
}
 

main函数不支持 参数 ,但是你可以os.Args获取命令行参数,比如下面的代码

package main
 
import (
	"fmt"
	"os"
)
 
func main() {
    if len(os.Args) > 1 {
        fmt.Println("6666===>", os.Args[1])
    }
}
 

运行

go run main chanex
# 上面的chanex 这个参数就会被拿进来了

变量以及数据类型

此内容的前提条件是:你的go mod 已经初始化完毕,我们先看看 变量, 使用你需要使用var 声明,这是可选的

及其简单的变量和常量

package test
 
import (
	"fmt"
	"testing"
)
 
// 知识点 变量声明
// 测试一个简单的 斐波那契 数列
var a = 0 // 全局的
 
func TestFbi(t *testing.T) {
	// 声明变量
	// a := 1 局部的
	b := 2
 
	for i := 0; i < 5; i++ {
		fmt.Println(" ", b)
		temp := a
		a = b
		b = temp + a
	}
}
 
func TestExchange(t *testing.T) {
	x := 1
	xx := 2
 
	x, xx = xx, x
 
	fmt.Println("---> 交换了")
	t.Log(x)
	t.Log(xx)
 
}
 

关于常量和Iota

看看常量使用,你需要使用const 声明就好了

 
package test
 
import "testing"
 
func TestXXX(t *testing.T) {
	t.Log("Testing")
}
 
//iota 是一个变量,它会在const 声明的地方 +1 操作 注意⚠️ _ 符号是省略
 
//下面的是一个面试题 问d是什么值 d是3
const (
    a = 1 + iota
    b
    _
    d
) 
 
// 依次使用 累加 +1
const (
	Monday = 1 + iota
	Tuesday
	Wednesday
)
 
// 我们使用位运算 来做判断
const (
	Readbale = 1 << iota
	Weitable
	Executable
)
 
func TestConstAdd(t *testing.T) {
	t.Log("Testing", Monday)
	t.Log("Tuesday", Tuesday)
}
 
// 使用与运算 它可以 非常建议的表示四种类型
func TestConstAdd2(t *testing.T) {
	testValue := 7 // 0011
	t.Log("Testing", testValue&Readbale)
	t.Log("Testing", testValue&Weitable)
	t.Log("Testing", testValue&Executable)
}
 
 

关于类型约束

go的基础数据类型如下图所示,go中是分32/64的

不允许出现 隐式类型转化

预定义值

math.MaxInt64
math.MaxFloat64
math.MaxUint32

定义自己的类型和取址

package test
 
import "testing"
 
type MyInt int64
 
func TestImplicit(t *testing.T) {
	var a int32 = 1
	var b int64
	b = int64(a)
 
	var c MyInt
	c = MyInt(b)
	t.Log(a, b, c)
}
 
func TestPoint(t *testing.T) {
	a := 1
	aPter := &a // 取地址符号  获取a的地址 引用(一般来说地址是16进制的)
	t.Log(a, aPter) 
	t.Logf("%T %T", a, aPter)
}
 
func TestString(t *testing.T) {
	var s string
	t.Log("*" + s + "*")
 
	//	判断空srtring 就是获取它的lengyh
	t.Log(len(s))
}
 

运算符和以及条件和循环

算数运算符 (注意哈 我们没有前置的++和--,只有后置++和--)

比较Array

比较Array如何比较?默认的情况下go是可以进行比较的,但是其他的语言是没法比较的他们比较的是地址引用

(先比较长度和维度 再比较值内容)

package test
 
import "testing"
 
func TestOperatorArray(t *testing.T) {
	a := [...]int64{1, 2, 3, 4}
	b := [...]int64{1, 2, 3, 3}
	c := [...]int64{1, 2, 3, 2}
	d := [...]int64{1, 2, 3, 4}
 
	t.Log(a == b, c == d, a == d)
}

按位清零运算符是啥?

按位清零运算符是啥?右边为1 的话 左边无论是啥都是0 ,否则保留左边的数 &^

package test
 
import "testing"
 
// 这个代码主要测试 点就 是 更改 程序的“可读 可写 可执行”的便捷更改
 
func TestConstAdd3(t *testing.T) {
	const (
		Readbale = 1 << iota
		Weitable
		Executable
	)
	testValue := 7 // 0111
	// 让它变成 不可写
	testValue = testValue &^ Readbale
	// 让它变成 不可读
	// testValue = testValue &^ Weitable
	// 让它变成 不可执行
	testValue = testValue &^ Executable
	t.Log("Testing", testValue&Readbale == Readbale)
	t.Log("Testing", testValue&Weitable == Weitable)
	t.Log("Testing", testValue&Executable == Executable)
}

基础循环结构

package test
 
import (
	"fmt"
	"testing"
)
 
func TestBaseFor(t *testing.T) {
	// 简单的for
	for i := 0; i < 5; i++ {
		t.Log(i)
	}
 
	// while循环
	a := 5
	for a < 5 {
		t.Log(a)
		a--
	}
 
}
 

基础条件语句

 
func TestIf(t *testing.T) {
	if v, err := someFunctiono(); err == nil {
		t.Log("没有出错", v)
	} else {
		t.Log("有出错", err)
	}
}
 
func someFunctiono() (sum int, err error) {
	a := 10
	fmt.Println(a)
	return a, nil
}
 

关于switch的骚操作

在if的语句中可以 写两端 第一端是赋值,第二段是判断 (出现这个的原因主要是go的function可以有多个返回值)

switch条件 可以不限制常量和整数以及数量,默认不需要break ,go是自动给你带的,case可以放具体的条件表达式

 
func TestSwitchMultiCase(t *testing.T) {
	for i := 0; i < 5; i++ {
		switch i {
		case 0, 2:
			t.Log("Even")
		case 1, 3:
			t.Log("Odd")
		default:
			t.Log("it is not 0-3")
		}
	}
}
 
func TestSwitchConditionCase(t *testing.T) {
	for i := 0; i < 5; i++ {
		switch {
		case i%2 == 0:
			t.Log("Even")
		case i%2 == 1:
			t.Log("Odd")
		default:
			t.Log("unknown")
		}
	}
}
 

数组和切片

数组

1.声明的几种方式,多维也是一样的

 
func TestNeweggArray(t *testing.T) {
	// 我们具备如下的声明方式
	arr1 := [4]int{1, 2, 3, 4}
	arr2 := [...]int{1, 2, 3, 4, 5}
	arr1[1] = 0
 
	t.Log(arr1[1], arr1[2])
	t.Log(arr2)
 
}

2.如何去便 利Array _是 省略的意思

 
 
func TestNeweggArrayOp(t *testing.T) {
	// 我们具备如下的 截取和便利取值 赋值 操作
	arr := [...]int{1, 3, 4, 5}
 
	// 类似js的 forEach 遍历 ,如果你希望省略 某值 ,只需要把它变成 _ 就可以了
	// for idx, e := range arr {
	for _, e := range arr {
		t.Log(e)
	}
}
 

3.Array的快速截取 “前包 后不包”

 
 
func TestNeweggArrayOp(t *testing.T) {
	// 快速 的截取
	// a[开始的索引(包含), 结束索引(不包含)]
	a := [...]int{1, 2, 3, 4, 5}
 
	t.Log(a[1:2]) // 2
	t.Log(a[1:3]) // 2,3
	t.Log(a[1:])  // 2,3,4,5
	t.Log(a[1:])  // 2,3,4,5
	t.Log(a[:3])  // 1,2,3
}
 

切片Slice 是什么

一句话:可变长的array,它具的结构如下图所示

指针指向内存中一片连续的存储空间

1.声明

// 实际上 slice 是一种结构 包含了 三个重要的 属性 1,地址,2. len  3, cap 容量
func TestSliceNew(t *testing.T) {
	t.Logf("TestSliceNewegg")
	// 我们看看如何 new 一个简单的 slice , 记住它是一个 可变长的 array
	var s0 []int
	t.Log(len(s0), cap(s0))
 
	// 追加一个如何追加,为什么需要重新 对s0 复制一遍?
	s0 = append(s0, 1)
	t.Log(len(s0), cap(s0))
 
	s1 := []int{1, 2, 3, 4}
	t.Log(len(s1), cap(s1))
 
	s2 := make([]int, 3, 5) // 使用 make 方法创建slice参数 1 是类型 ,参数2是len  ,参数3是容量
	t.Log(len(s2), cap(s2))
	t.Log(s2[0], s2[1], s2[2]) // 注意⚠️  不能访问 len  ,外的 值 会报错!,例如==> s2[4]
 
}
 
 

2.观察一下其cap容量的增长规律

// 我们看看 slice 的 容量的cap 的增长规律
func TestSliceNewLeng(t *testing.T) {
	s := []int{}
 
	for i := 0; i < 10; i++ {
		s = append(s, i)
		t.Log("--->", len(s), cap(s))
		// 通过 图 我们可以看到 cap 的可变范围 都是 * 2 的增长的
	}
}

3.用切片 共享的存储结构

这里需要说明的months 这个 array 再内存中是存在的 sliceQ2 和 sumer 如果指向 了这个Array 那么他们的修改 会直接彼此互相影响,因为 指针指向的引用没变

 
func TestSlicShaerMemory(t *testing.T) {
	year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
	Q2 := year[3:6]
	t.Log(Q2, len(Q2), cap(Q2))
 
	sumer := year[5:8]
	t.Log(sumer, len(sumer), cap(sumer))
 
	// 用于 是引用 将会导致 值的同步变更
	sumer[0] = "unknown"
	t.Log(Q2, year)
 
}
 

总结Arry 和 slie 对比二者的区别

  • array 容量不可变 slice 可以
  • array 可直接比较 slice 不可以

Map

基础的使用

package test
 
import (
	"testing"
)
 
// 如何进行声明(cap不能用len可以使用)
func TestIniitMap(t *testing.T) {
	m1 := map[int]int{1: 1, 2: 4, 3: 9}
	t.Log(m1[2])
	t.Logf("len m1=%d", len(m1)) // f 可以组合模板字符串
 
	m2 := map[int]int{}
	m2[4] = 16
	t.Logf("len m2=%d", len(m2))
 
	m3 := make(map[int]int, 10)
	t.Logf("len m3=%d", len(m3))
}
 
// 访问有一个不存在的值 0 和 值0,我们如访问0 值 和不存在呢?
func TestAceessNotEXistingKey(t *testing.T) {
	m1 := map[int]int{}
	t.Log(m1[1])
	m1[2] = 0
 
	// 注意值不存在的时候是一个 0 ,key存在value = 0 的时候也是o 这个时候我们需要这样判断
	if _, ok := m1[3]; ok {
		t.Logf("key 3's value is %d", m1[3])
	} else {
		t.Log("key 3 is not existing")
	}
}
 
// 如何去遍历一个Map
func TestFoEatch(t *testing.T) {
	m1 := map[int]int{1: 1, 2: 4, 3: 9}
	for key, value := range m1 {
		t.Log(key, value)
	}
}
 

进阶操作

// 存储function
 
 
func TestMapFunC(t *testing.T) {
	m := map[int]func(op int) int{}
	m[1] = func(op int) int { return op }
	m[2] = func(op int) int { return op * op }
	m[3] = func(op int) int { return op * op * op }
 
	t.Log(m[1](2), m[2](2), m[3](2))
}
 
//我们可以使用map来实现set
func TestMapForSet(t *testing.T) {
	set := map[int]bool{}
	set[1] = true
 
	// hash 可以这样实现
	n := 1
	if set[n] {
		t.Log("存在")
	} else {
		t.Log("不存在")
	}
 
    
	// delete可以这样实现
	delete(set, n)
	if set[n] {
		t.Log("存在")
	} else {
		t.Log("不存在")
	}
}
 

字符串

关于字符串的重要概念

第一点我们先来聊聊Unicode 和byte 的问题

注意:string 零值是一个 0 哈,因为它是一种数据类型,go中的数据类型基本上都是默认值就是0

string 的byte的 存任何二进制的数据

string是 一个不可变的byte slice

Unicode UTF8的区别,Unicode是编码规则,UTF8是这种编码规则的具体实现,rune取Uncode编码

func TestStringS(t *testing.T) {
	var s string
 
	t.Log(s) // 初始化默认值 = 0
	s = "hello"
	t.Log(len(s)) // 对于英语来说 一个字母算一个
	// s[1] = "3" 不能这样写string的本质是一个不可变的 byte slice
	s = "\xE4\xB8\xA5" // 存任何 二进制的数据 这个二进制编码代表 严
	// s = "\xE4\xB8\xA5\xFF" // 存任何 二进制的数据 这个是一个乱码
	t.Log(s)
	t.Log(len((s)))
 
	s = ""
	t.Log(len((s))) // 获取的是byte数
 
	c := []rune(s)               // rune slice 表示的是unicode slice
	t.Logf("中 unicode %x", c[0]) // 使用16 进位制 去获取 0位置上的 unicode编码
	t.Logf("中 UTF %x", s)        // 使用16 进位制 去获取 s
}
 

关于字符串的常用方法和一些库

 
func TestStringFN(t *testing.T) {
	s := "A,B,C"
	parts := strings.Split(s, ",")
	t.Log(parts)
 
	for _, part := range parts {
		t.Log(part) // 遍历 string
	}
	t.Log(strings.Join(parts, "-")) // 组合
}