文章目录
前言
这篇博客完全是因为对go突然产生了兴趣才开始的,无奈实习岗位太少,暑期还是准备去做java全栈了,虽然都说java卷(更吃学历了),但是go生态真的暂时落后太多,所以还是老老实实去做java了,go之后有缘再做吧,(大厂的话内部转go,尤其是java转go,是挺常见的,尤其是字节)。
也是考虑了很多,最后还是选了java。实习打开boss、实习僧,清一色的java,go岗位还是很有限的(其实也还好,不过java真的多)。问了一些学长学姐,大家本科出来也都是走的后端开发岗,身边也有部分走数据分析的(运维、测试就算了)。走嵌入式方向,硬件也没基础,据说今年嵌入式卷到飞起,还是老老实实开发岗比较合适。c++游戏开发,也是很可以的,还有一些时间,如果下学期找不到实习的话也会去做一两个游戏项目,也不能太all in java吧。
这篇博客前面部分还是自己学过来的,后面到了高级应用是一塌糊涂,拿了很多人家的博客内容,勉强拼凑完了,唉,还是开始java吧,不然开学实习就不好投了,希望之后还有精力分享java学习和实习面试内容。
1. go语言优势
- 可直接编译成机器码,不依赖其他库。
- 静态类型语言是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高。
- 语言层面支持并发,这个就是Go最大的特色,天生的支持并发。Go就是基因里面支持的并发,可以充分的利用多核,很容易的使用并发。
- 内置runtime,支持垃圾回收,这属于动态语言的特性之一吧,虽然目前来说GC(内存垃圾回收机制)不算完美,但是足以应付我们所能遇到的大多数情况,特别是Go1.1之后的GC。
- 简单易学,Go语言的作者都有C的基因,那么Go自然而然就有了C的基因,那么Go关键字是25个,但是表达能力很强大,几乎支持大多数你在其他语言见过的特性:继承、重载、对象等。
- 丰富的标准库,Go目前已经内置了大量的库,特别是网络库非常强大。
- 内置强大的工具,Go语言里面内置了很多工具链,最好的应该是gofmt工具,自动化格式化代码,能够让团队review变得如此的简单,代码格式一模一样,想不一样都很困难。
- 跨平台编译,如果你写的Go代码不包含cgo,那么就可以做到window系统编译linux的应用,如何做到的呢?Go引用了plan9的代码,这就是不依赖系统的信息。
- 内嵌C支持,Go里面也可以直接包含C代码,利用现有的丰富的C库。
2. go语言不足
- 泛型缺乏: 尽管在Go 1.18版本中引入了泛型,但其功能相对其他语言仍然有限,可能不够成熟,使用起来也不如C++或Java中的泛型灵活。
- 错误处理: Go采用显式的错误返回机制,而不是异常处理。这种方法虽然简单明确,但在处理复杂的错误逻辑时,代码显得繁琐且冗长,容易导致大量的重复代码。
- 依赖管理: 尽管Go模块系统(Go Modules)已经有所改进,但在处理依赖管理方面,尤其是版本控制和依赖冲突上,仍然存在一些问题。
- 不支持动态加载: Go不支持像Java那样的动态类加载,这在某些需要动态扩展或插件化的应用场景中会显得不够灵活。
- 生态系统较小: 相对于Java、Python等流行语言,Go的生态系统和第三方库相对较少,特别是在一些特定领域,这点相比java来说是很大的劣势。
- GUI开发支持有限: Go在桌面应用程序的开发支持上较为有限,虽然有一些第三方库,如fyne和Qt bindings,但整体上不如Java或C#等语言的GUI开发工具成熟。
3. go适合用来做什么
- 服务器编程,以前你如果使用C或者C++做的那些事情,用Go来做很合适,例如处理日志、数据打包、虚拟机处理、文件系统等。
- 网络编程,这一块目前应用最广,包括Web应用、API应用、下载应用。
- 内存数据库,如google开发的groupcache,couchbase的部分组建。
- 云平台,目前国外很多云平台采用Go开发,CloudFoundy的部分组建,前VMare的技术总监自己出来搞的apcera云平台。
- 分布式系统,数据库代理器等。
4. 环境搭建与编译器
这个可以看其他文章(点击访问网址),go的环境变量配置中,goroot是go的安装目录,相当于java中的jdk,gopath是go的项目工作目录,存放项目代码,个人比较推荐用vscode来编写go语言程序。
第一章 基础语法
1. Hello word!
go代码需要声明package包名,通过import引入我们的库(第三方库或者自定义库)。其中package main表示一个可独立执行的程序,每个 go 应用程序都包含一个名为 main 的包(并不是包名叫做main)。
func main()是程序开始执行的函数。main 函数是每一个可执行程序所必须包含的,一般来说都是在启动后第一个执行的函数(如果有 init() 函数则会先执行该函数)
注意:这里面go语言的语法,定义函数的时候,‘{’ 必须和函数名在同一行,不能另起一行。
package main
import (
"fmt"
)
func main() {
//不能另起一行
fmt.Println("Hello word!")
}
2. 变量声明
go支持匿名变量,用_
(空白标识符)来代替变量,比如:_, str := test()
,test()返回两个值。
- 指定变量类型
var a int=0
(没有赋值则为默认值) - 不指定变量类型
var a=0
(自动识别) - 省略var关键字
a :=0
(这种不可以用于声明全局变量)
多变量声明:
var ( //这种分解的写法,一般用于声明全局变量
a int
b float32
)
var aa,bb = 123, "go"
3. 常量(const和iota)
const在c++经常使用,在go语言中同样表示常量,即不可修改的变量。
- 显式类型定义:
const b string = "abc"
(是否声明变量类型) - 隐式类型定义:
const b = "abc"
常量可以计算表达式的值len(),cap(),unsafe.Sizeof()等,也就是说常量可以作为参数被传入到这些函数中。
const (
a = "abc" //abc
b = len(a) //3
c = unsafe.Sizeof(a) //16
)
//unsafe.Sizeof(a)输出的结果是16
//字符串类型在 go 里是个结构, 包含指向底层数组的指针和长度,
//这两部分每部分都是 8 个字节,所以字符串类型大小为 16 个字节。
//组成可以理解成此结构体
type string struct
{
Data uintptr // 指针占8个长度
Len int // 长度64位系统占8个长度
}
注:常量表达式中,函数必须是内置函数。
常量声明可以使用 iota 常量生成器初始化,它用于生成一组以相似规则初始化的常量。在一个 const 声明语句中,在第一个声明的常量所在的行,iota 将会被置为 0,然后在每一个有常量声明的行加1。有点像我们c++的迭代器,每次都会进行自增。
const (
Unknown = 0
Female = 1
Male = 2
)//普通的const枚举
//采用iota进行枚举
const (
a = iota //0
b //b=1 ,iota = 1
c //c=2, iota = 2
d = "ha" //独立值,结果为ha,iota=3
e //"ha", iota=4
f = 100 //iota =5
g //100 iota =6
h = iota //7,恢复计数
i //8
j = "nihao"//nihao iota=9
k = "shijie"//shijie iota=10
l = iota//11
m //12
)
4. 函数
4.1 函数结构
在go中,函数名称首字母大写是公有方法,首字母小写是私有方法,变量也是。
不像c++和java通过public,private等关键字来对变量和函数进行修饰以控制作用域。
基本的结构如下:
func Name(/*参数列表*/) (a type1, b type2/*返回类型*/) {
//函数体
return v1, v2 //返回多个值
}
根据参数和返回值有无,简单举下面几个例子,这个也和其他语言类似:
func Test1(v1 int, v2 int) {
//有参无返回
//或者func Test02(v1, v2 int)
fmt.Printf("v1 = %d, v2 = %d\n", v1, v2)
}
//形如...type格式的类型只能作为函数的参数类型存在,并且必须是最后一个参数
func Test2(args ...int) {
//可变参数,传入若干个int型变量
for _, n := range args {
//忽略索引,遍历参数列表
fmt.Println(n)
}
}
多个返回值:
func Test01() (int, string) {
//方式1
return 1, "go"
}
func Test02() (a int, str string) {
//方式2, 给返回值命名
a = 1
str = "go"
return
}
init()函数,构造函数,和其它语言执行顺序一样,不多赘述。
main函数,只能在package main中。
注:一个包会被多个包同时导入,那么它只会被导入一次,也就是只执行一次init()。
4.2 函数传参
两种传参方式,值传递和引用传递,和c++的传参机制类似,go的传参还是很简单的。
- 值传递是指在调用函数时将实际参数复制一份传递到函数中,不影响实际参数。
- 引用传递(指针传递)
函数中引用传递用*
变量指向地址用&
4.3 匿名函数和闭包
这个很重要!
匿名函数是指没有名字的函数,可以在声明的同时定义和调用。匿名函数非常适合用于需要一次性使用的函数,或者将函数作为参数传递给其他函数的场景。
package main
import "fmt"
func main() {
// 定义并调用一个匿名函数
func(message string) {
fmt.Println(message)
}("Hello, World!")//()的作用是,此处直接调用此匿名函数
}
闭包是指函数可以捕获并记住其作用域外的变量。闭包使得这些变量的值在函数调用后仍然保持不变,或者在不同的函数调用间共享这些变量的状态。
或者理解为定义在一个函数内部的函数,是将函数内部和函数外部连接起来的媒介。
package main
import "fmt"
func main() {
// 创建一个闭包
counter := func() func() int {
//函数名为func(),返回值为func() int,传参为null
count := 0
return func() int {
count++
return count
}
}()//()的作用是,此处直接调用此匿名函数
// 调用闭包
fmt.Println(counter()) // 输出: 1
fmt.Println(counter()) // 输出: 2
fmt.Println(counter()) // 输出: 3
}
count变量的生命周期却不会因为函数执行完而结束。这是因为count变量在内函数外部定义,被这个匿名函数所引用,所以它会在整个程序的运行期间保留下来。
5. 类型转换
go语言中不允许隐式转换,所有类型转换必须显式声明,而且转换只能发生在两种相互兼容的类型之间。
var ch byte = 97
//var a int = ch //err, cannot use ch (type byte) as type int in assignment
var a int = int(ch)
类型别名
type bigint int64 //int64类型改名为bigint
var x bigint = 100
type (
myint int //int改名为myint
mystr string //string改名为mystr
)
6. defer关键字
defer语句被用于预定对一个函数的调用。可以把这类被defer语句调用的函数称为延迟函数。
什么意思呢?
c++的析构函数,是在函数生命周期结束后执行的函数。defer也类似,相当于延迟执行函数。
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行,包括导包的顺序,就类似于析构函数,defer的执行在return语句之后。
注意,defer语句只能出现在函数或方法的内部。
defer语句经常被用于处理成对的操作,如打开、关闭、连接、断开连接、加锁、释放锁。
通过defer机制,不论函数逻辑多复杂,都能保证在任何执行路径下,资源被释放。
释放资源的defer应该直接跟在请求资源的语句后。
func main() {
a, b := 10, 20
defer func(x int) {
// a以值传递方式传给x
fmt.Println("defer:", x, b) // b 闭包引用
}(a)
a += 10
b += 100
fmt.Printf("a = %d, b = %d\n", a, b)
/*
运行结果:
a = 20, b = 120
defer: 10 120
*/
}
7. 数组与slice
go的数组定义var a[10] int {1, 2, 3}
,但是定长数组在函数传参时需要声明长度,a[10] int 和 a[4] int是不同的变量,而且这样只是浅拷贝(值拷贝),这些是传参时的区别。
还有数组遍历,可以用for也可以用range函数,这个前面有讲,返回索引(下标)和数组元素值,常使用匿名变量来丢掉索引值。
7.1 定义切片
slice(切片),可以认为是动态数组。slice定义比较简单,有点像c语言需要用malloc()动态分配内存。而且slice传参是引用传递,就像在c++中,数组本质上就是指针。
//1. 直接创建,只需要中括号内不声明元素个数就默认动态数组
var a []type
//2. 声明数组,使用make()函数来开辟空间,有点像c语言
var slice1 []type = make([]type, len)
//也可以简写为:
slice1 := make([]type, len)
//3.也可以指定容量,其中capacity为可选参数
make([]T, length, capacity)
切片扩容是以cap为单位进行扩容,超过cap容量时,会自动扩展成2*cap。
7.2 切片初始化
//1. 直接初始化切片,[]表示是切片类型,{1,2,3}初始化值依次是1,2,3.其cap=len=3
s :=[] int {
1,2,3 }
balance := [...]float32{
1000.0, 2.0, 3.4, 7.0, 50.0}//也可以用...表示
balanced := [5]float32(1:2.0, 3:7.0)//声明部分元素
//2. 初始化切片s,是数组arr的引用
s:=arr[:]
//3. 将arr中从下标startIndex到endIndex-1 下的元素创建为一个新的切片,首位若缺省就是默认
s := arr[startIndex:endIndex]
//4. 通过切片s初始化切片s1
s1 := s[startIndex:endIndex]
//5. 通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片,cap是容量
s :=make([]int,len,cap)
7.3 切片常用操作
① len() :获取切片长度。
对切片进行截取时,可以认为是通过指针来实现的,不是浅拷贝。
② cap() :测量切片最长可以达到多少。
一个切片在未初始化之前默认为 nil(类似于null),长度为 0。
③ 切片截取:可以通过设置下限及上限来设置截取切片[lower-bound:upper-bound]。
package main
import "fmt"
func main() {
/* 创建切片 */
numbers := []int{
0,1,2,3,4,5,6,7,8}
printSlice(numbers)
/* 打印原始切片 */
fmt.Println("numbers ==", numbers)
/* 打印子切片从索引1(包含) 到索引4(不包含)*/
fmt.Println("numbers[1:4] ==", numbers[1:4])
/* 默认下限为 0*/
fmt.Println("numbers[:3] ==", numbers[:3])
/* 默认上限为 len(s)*/
fmt.Println("numbers[4:] ==",