golang初学

前言

一门语言不够那就学两门,对于完全没学过go但是学过其它语言且有一定的开发经验的我来说,感觉两天学完基本语法没问题。

安装报错

当我去官网下载了go的最新版本的安装程序后,运行后发生这么一个错误:
image-20260612191104318

这是典型的MSI安装包权限或系统服务异常问题。

然后我这么做:

  • 缺少Windows Installer文件夹
    打开“此电脑” → C盘 → Windows文件夹 → 检查是否存在名为 Installer 的隐藏文件夹。若不存在,手动新建一个名为 Installer 的文件夹(注意:不要加引号,不要带空格)。

后就成功了,那么这个文件夹是什么?

C:\Windows\Installer 文件夹是 Windows Installer 服务用于存储软件安装、更新、修复和卸载所需缓存文件的核心目录,其作用是确保系统能正确管理软件生命周期。

基础语法

当我要运行main文件时,我可以在命令行运行go run main.go;或者运行go build main.go,这样会生成main.exe,然后.\main.exe运行这个可执行文件,这相当于把编译执行拆开。

我每次修改完代码后,都需要ctrl+s保存,不然终端看的还是旧代码。

短变量声明法:

1
2
3
4
5
6
func main() {

a, b, c := 1, 2, "3"
fmt.Println(a, b, c)
// fmt.Println("a", "a")
}

这样可正常打印:1 2 3(中间有一个空格,这是规范,就算println中不夹空格这里也会加),然后短变量声明不能用来声明全局变量

匿名变量:

在使用多重赋值时,如果想要忽略某个值,可以使用匿名变量(anonymous variable)。 匿

名变量用一个下划线_表示,例如:

1
2
3
4
5
6
7
8
func getUserinfo (string, int){
return "ZhangSan", 18
}

func main(){
var _, age = getUserinfo()
fmt.Println(age)
}

匿名变量不占用命名空间,不会分配内存,所以匿名变量之间不存在重复声明。

注意事项:

1、函数外的每个语句都必须以关键字开始(var、const、func 等)

2、:=不能使用在函数外。

3、_多用于占位,表示忽略值。

基本数据类型及其范围:

先记一下整形的数据范围

image-20260613100426586

然后我们声明变量的时候就可以显示定义某个数是哪个范围的,就像这样:

1
2
3
4
5
func main() {
var num int8
num = 123
fmt.Printf("值:%v 类型%T", num, num)
}

会打印出:
值:123 类型int8

此时要注意,由于int8类型的范围是-128到127,所以如果赋值为128会报错。

如果是直接var num int = 128,那么此时的数据类型根据操作系统架构而定,如果是64位系统,int就默认是int64。

那么为防止数据超出范围,为什么不都直接用int呢?因为int比int8等更小位数的数据占用的字节更多,更不省内存。

只要数据大小不溢出,各数据类型可相互转换:
image-20260613101028606

再记一下各转义字符输出数据:
image-20260613101400404

字符属于int型:它的值为ascii值

image-20260613112245051

go中的字符分为两种类型。
一种是byte,也叫uint8类型类型,就是输出“我”时输出的是三个我现在还不认识的符号,因为它在utf-8编码下占据三个字节
另一种是rune类型, 按%v(%c)格式输出时就是正常的汉字和它对应的ascii值,例如20320(你)

二者区别在于输出汉字时,能否直接输出汉字原型。

值类型:

基本数据类型和数组都属于值类型,值类型有一个特点就是:

当数组a1被声明了,然后数组a2 := a1,那么此时a2(副本)是额外开辟了内存空间的,此后a1的修改于a2无关,类似于浅拷贝

引用类型:

就是与上面相对的,副本会改变的,一般切片就是引用类型,存在内存空间共享

函数作为类型

我知道函数可以作为参数,其实函数也能作为数据类型。

image-20260613165654508

如图,第一行通过type关键字声明了一个calc类型的函数,那么我在主函数中就可以声明一个calc类型的函数c,然后c就可以复制为函数sub,因为sub是完全符合calc的参数及返回要求的,这里很像python中的多态。

闭包:可以让变量常驻内存又不污染全局

闭包就是函数里面再写个函数,然后外层函数返回值是内层函数。在主函数中,当外层函数被调用并被传入了一个参数,此时它返回一个内层函数,然后外层函数的局部变量相对于内层函数就是全局变量,当内层函数对该变量进行修改时,该变量会被作为全局变量而改变,所以如果下一行又调用了这个内层函数,那么此时的内层函数是在上一行改变了的变量的条件下进行作用的。(这一段很抽象,只有我编辑的时候能看懂,所以上图!)

image-20260614161730289image-20260614161744451

异常处理

go中如果没有特异的异常处理代码,遇到错误时会直接结束执行,不会继续往后。

下面给出defer、panic、recover抛出异常的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func readFile(fileName string) error {
if fileName == "main.go" {
return nil
}
return errors.New("读取文件错误")
}
func fn3() {
defer func() { //defer会被注册并压入defer栈中,在这个函数结束前执行
err := recover() //用recover获取异常
if err != nil {
fmt.Println("抛出异常给管理员发送邮件")
}
}() //自执行函数
var err = readFile("xxx.go")
if err != nil {
panic(err) //主动抛出异常
}
fmt.Println("继续执行")
}
func main() {
fn3()
}

这段代码只会打印:
抛出异常给管理员发送邮件

因为抛出异常时,函数会准备退出,但是退出之前要先执行defer语句,然后recover() 成功捕获了 panic 传递过来的错误信息。

指针巩固理解

变量a的值为10,程序运行时,a存在于内存中,且有一个内存中的地址,然后可以令p := &a,此时指针p的值就是a的地址,同时呢,p自身也有一个地址,这个地址是和a的地址没有关系的,也能用取地址符号&获得p的地址。

结构体

结构体方法:

结构体函数有下面这两种定义方式image-20260615155438477

第二种的接收者类型为指针,这样的方式使得在主函数中调用这个方法时,属性可以全局改变,但是第一种就不行。

使用第三方包

假如我的项目目录最后是Test(项目名)

那么我应该先在命令行用go mode init 项目名 来初始化项目,这一步会生成go.mod文件用于项目包管理。

当我要自己写一个包时,我应该先新建一个calc文件夹在根目录下,然后在calc文件夹下写函数,就像这样:
image-20260615193414892

然后引用时就这样:
image-20260615193444613

这是用自己的包的流程,接下来说用官方包:

要先去网站Go Packages - Go Packages中找到自己要的那个包

然后import左上角这个包的地址image-20260615193731175

,然后要么用go mod download 命令下载,要么就用go mod tidy来自动补全缺失的包(这个好用一点),然后就能用了

接口

Go中,把接口理解为一种约定,里面会声明一些函数,然后这些函数依赖结构体来实现(结构体需要实现约定中的所有函数)。然后在主函数中,声明一个接口实例和一个结构体实例,把结构体实例赋值给接口实例,此时接口实例就能调用接口中约定的方法了。

空接口

给它赋值的变量是什么类型,那么这个接口就是什么类型。

类型断言

当我要判断一个接口实例是什么类型时。
第一种用法:用语法flag := a.(string),相当于判断实例a是否时string类型,这是一个布尔变量。
第二种用法:a.(type),然后用switch case来分类讨论。

接口到底有啥用

这么说接口其实可以不用写,它可以理解为只是一个标注作用,当项目中的某个结构体实现了接口定义的所有方法,它就实现了这个接口,实际上此时那个结构体还和接口没有关系。但是当有一个函数要传入一个接口变量是,我就可以传入那个实现了接口的结构体了对吗?对,但是接口还是有重要意义的,就像这样:

1
2
3
4
5
6
7
8
9
10
11
// ============ 没有接口的世界 ============
func MakeDogSound(d *Dog) { fmt.Println(d.Speak()) } // 只能传 Dog
func MakeCatSound(c *Cat) { fmt.Println(c.Speak()) } // 只能传 Cat
func MakeRobotSound(r *Robot) { fmt.Println(r.Speak()) } // 只能传 Robot
// ↑ 每多加一种动物,就要多写一个函数

// ============ 有接口的世界 ============
type Speaker interface { Speak() string } // ← 这行才是接口的真正价值

func MakeSound(s Speaker) { fmt.Println(s.Speak()) } // 一种动物都不用改
// ↑ 接口让 MakeSound 和具体动物类型解耦了

协程

协程是用户级的一个操作,比如程序中的一个函数,在主协程中(main.go)只需要一个go关键字就能开辟一个协程。当主协程执行完毕时,无论其它协程有没有执行完,都会停止。

管道

用于各个协程之间通信(各协程可以共享管道)

互斥锁与读写互斥锁

这俩东西不一样,读写互斥锁中,读操作是可以并发的,但是写操作要独占一个锁

反射

初步理解为给函数传入一个参数,定义函数时用空接口表示要接收的参数,这样的话实际使用时可以接收任意数据类型的接口,然后可以通过反射包中的方法获取传入参数的类型以及值。获取到后就可以分情况修改或者操作这个参数了。

文件

读文件时,每次可以选择读一行或者多少字节,然后for循环读取,读完后会有个err参数接收到信息,用if判断这个参数,然后跳出循环。

写文件时,有个参数配置:
image-20260618164742337

泛型

1
2
3
func getData[T any](value T) T {
return value
}

这样的话,传入的 value可以是任何类型,不需要做类型断言,也不用担心类型安全

Icon
致谢名单
本作品由 Hafsun 于 2026-06-12 18:49:10 发布
作品地址:golang初学
除特别声明外,本站作品均采用 CC BY-NC-SA 4.0 许可协议,转载请注明来自 欢迎回家
Logo
上一篇gozero初学下一篇肖佬经验之谈