这是我参与「第五届青训营」伴学笔记创作活动的第 1 天
本系列文章试图从一名 Java 开发者(有时也会穿插其他语言)的角度窥探 Go 语言,并以注释的方式提及 Go 与 Java 的一些区别,方便 Java 开发者迅速入门 Go 语言。
与 Java 相同,Go 是一门高性能,高并发,语法简单,学习曲线平缓的强类型和静态类型语言,其拥有丰富的标准库,完善的工具链,支持快速编译,跨平台且支持垃圾回收(GC);
与 Java 不同的是,其并不是一门虚拟机语言,不需要通过中间代码表示(例如 JVM Bytecode)和虚拟机(VM)支持代码运行,其以直接将目标代码静态链接并编译到目标平台的形式跨平台。
虽然 Go 和 C/C++ 类似,人们也经常讲 Go 讲述为“更好的 C/C++”,但 Go 的竞争领域并不是 C/C++ 所适合的领域,相反,Go 更适合 Java 所适合的 Web 工程等领域。理论上,Go 可以提供比 Java 更好的性能和吞吐量。
Go 是一门由 Google 主导开发的语言,目前已经更新至 1.19 版本。
要想开发 Go 程序,则需要 Go 开发环境,可以前往 Go 官网 并遵循 安装文档 安装对应平台的 Go 开发环境。这些开发环境包括 Go 编译器,工具和库。和 Java 不同的是,不存在类似于 JRE(Java Runtime Environment)一样的东西,用户可以直接运行编译后对应平台的可执行文件,无须运行时支持。
接下来,我们当然还需要 IDE 来便捷我们的开发。有两种主流 IDE 可选:VSCode 和 GoLand。前者是由微软开发的开源代码编辑器,后者则是由 Jetbrains 公司开发,基于著名 Java IDE IntelliJ IDEA 构建的功能强大的 IDE。
此两种 IDE 的区别是,前者更像手动挡,后者则是自动挡。对于进阶需求,VSCode 为你带来的可自定义性会更强;但是对于新手,个人还是推荐使用 GoLand。
值得一提的是,GoLand 是一款付费软件,在购买前,你有机会进行 30 天的使用;或者,如果你是一名在校大学生,你可以向 Jetbrains 申请一份免费的教育许可证,其允许你在学业期间免费使用 Jetbrains 的全套工具链;如果你已申请并通过 GitHub Education 学生包,那么你也可以通过此学生包获得 Jetbrains 教育许可证。
package main
import (
"fmt"
)
func main(){
fmt.Println("hello world")
}
以上是使用 Go 语言输出 Hello World 的代码。可以看出,Go 语言的入口点是 main
函数(注意 Go 语言同时存在函数和方法,前者可以认为是 Java 的静态方法或者 Rust 的关联函数,后者可以认为是非静态方法);除此之外,fmt.Println
类似于 System.out.println
,可将一段数据打印在标准输出流中。
应当注意到,在 Go 语言中,;
不是必要的,当一行中只存在一个语句时,则不必显式的为语句末添加 ;
。
你可能注意到,Println
中的 P
是大写的,你可能会主观的认为这是 Go 语言的命名习惯,就像 C# 开发者那样。但实际上,在 Go 语言中,函数/方法首字母大写意味着可被其他包调用,否则只能在该包被调用,这就类似于 Java 中 public
和 protected
访问修饰符的区别。
与 Java 不同,Go 语言的变量是类型后置的,你可以这样创建一个类型为 int
的变量:
var a int = 1
当然,允许在同一行声明多个变量:
var b,c int = 1, 2
Go 支持变量类型自动推断,也就是说,当我们立即为一个变量进行初始化时,其类型是可以省略的:
var d = true
相反,如果我们未为一个变量初始化,则必须显式指定变量类型,此时,变量会被以初始值自动初始化:
var e float64 // got 0
可以通过 :=
符号以一种简单的方式(也是实际上最常用的方式)声明一个变量:
f := 3.2 // 等价于 var f = 3.2
最后,可以使用 const
关键字代替 var
关键字来创建一个常量(不可变变量):
const h string = "constant"
对于流程控制这一部分,其实各语言都大差不差,所以就简略讲讲。
Go 支持 if
,else if
,else
, switch
进行选择控制。
if 7%2 == 0 {
fmt.Println("7 is even")
} else {
fmt.Println("7 is odd")
}
if num := 9; num < 0 {
fmt,Println(num,"is negative")
} else if num < 10 {
fmt.Println(num, "has 1 digit")
} else {
fmt.Println(num, "has mutiple digits")
}
你可能会注意到,其他语言中,if
(其他类似)后应当紧跟一个括号,括号内才是表达式,但是在 Go 中,这个括号是可选的,我们也建议不要使用括号。
要注意的是,if
表达式后面的括号是必需的,即使是对于单行语句块,您也必须添加括号,而不能像其他语言那样直接省略。
a := 2
switch a {
case 0, 1:
fmt.Println("zero or one")
case 2:
fmt.Println("two")
default:
fmt.Println("other")
}
这便是最简单,也是和其他语言最相似的 switch
语句,对一个 a
变量进行扫描,并根据不同的值输出不同的字符串。
当然,你也可以直接省略 switch
后的变量,来获得一个更加宽松的 switch
语句:
t := time.Now()
switch {
case t.Hour() < 12:
fmt.Println("It's before noon")
default:
fmt.Println("It's after noon")
}
需要注意的是,与其他语言恰好相反,switch
语句中每个 case
的 break
是隐式存在的,也就是说,每个 case
的逻辑会在执行完毕后立刻退出,而不是跳转到下一个 case
。
要想跳转到下一个 case
,则应该使用 fallthrough
关键字:
v := 42
switch v {
case 100:
fmt.Println(100)
fallthrough
case 42:
fmt.Println(42)
fallthrough
case 1:
fmt.Println(1)
fallthrough
default:
fmt.Println("default")
}
// Output:
// 42
// 1
// default
需要注意的是,fallthrough
关键字只能存在于 case
的末尾,也就是说,如下做法是错误的:
switch {
case f():
if g() {
fallthrough // Does not work!
}
h()
default:
error()
}
但是,你可以使用 goto
+ 标签的方式来变相的解决这个问题。但是由于 goto
无论在任何语言的任何地方都应当是不被推荐使用的语法,因此此处不作继续探讨。想要继续了解的可以前往 Go Wiki 查看。
在 Go 语言中不区分 for
和 while
。你可以通过这样的方式创建一个最普遍的 for
语句:
for j := 7; j < 9: j++ {
fmt.Println(j)
}
或者,将 for
语句中的三段表达式改为一个布尔值表达式,即可得到一个类似于其它语言的 while
语句:
i := 1
for i <= 3 {
fmt.Println(i)
i = i + 1
}
又或者,不为 for
语句填写任何表达式,你将得到一个无限循环,除非使用 break
关键字跳出循环,否则这个循环永远也不会停止,这看起来有些类似于 Java 的 while(true) {}
或是 Rust 的 loop {}
:
for {
fmt.Println("loop")
}
当然,我们也可以使用 for range
循环的方式来遍历一个数组,切片,集合乃至映射(Map
)。
当我们使用 for range
语句遍历一个数组,切片或是集合的时候,我们将得到该集合元素的索引(idx
)和对应值(num
):
nums := []int{2, 3, 4}
sum := 0
for idx, num := range nums {
fmt.Println("range to index:", idx)
sum += num
}
// Will got following output:
// range to index: 0
// range to index: 1
// range to index: 2
// sum: 9
fmt.Println("sum:", sum)
或者,当我们遍历一个 Map
时,将得到键(k
)和值(v
):
m := make(map[string]int)
m["hello"] = 0
m["world"] = 1
// If key and value both needed
for k, v := range m {
// Will got following output:
// key: hello, value: 0
// key: world, value: 1
fmt.Printf("key: %v, value: %v\n", k, v)
}
// Or only need key
for k := range m {
// Will got following output:
// key: hello
// key: world
fmt.Printf("key: %v", k)
}
如果我们不需要循环中的某个值,则可以使用 _
符号代替变量名来遮蔽该变量(其他语言也有类似的做法,但是在 Go 中,此操作是必须的,因为未被使用的变量或导入会被 Go 编译器认为是一个 error
):
// When only `v` variable needed
for _, v := range m {
//...
}
Go 语言没有 do-while
循环或其平替。可以通过这种方式手动编写一个近似的 do-while
循环:
for {
work()
if !condition {
break
}
}
很显然,break
和 continue
都是支持的,其用法和其他语言完全相同,在此直接略过。
可以使用以下方式声明一个指定长度的数组:
var a [5]int
a[4] = 100
声明了一个名为 a
,大小为 5 的 int
数组,并将其最后一个元素的值设置为 100
。
直接使用 :=
进行声明当然也是可行的:
b := [5]int{1, 2, 3, 4, 5}
声明了一个名为 b
,大小为 5,数组内元素初始值为 1,2,3,4,5
的 int
数组。
当然,多维数组也是可以的:
var twoD [2][3]int
创建了一个名为 twoD
的二维数组。
值得一提的是,当一个数组未被显式初始化元素值时,将采用元素默认值填充数组。
可以这样使用索引从数组中取出一个值:
fmt.Println(b[4]) // 5
当我们试图访问一个超出数组长度的索引,编译器将会拒绝为我们编译,并返回一个编译错误:
fmt.Println(b[5]) // error: invalid argument: index 5 out of bounds [0:5]
数组是定长的,因此在实际业务中使用的并不是很多,因此,更多情况下我们会使用切片代替数组。
就像它的名字一样,切片(slice
)某个数组或集合的一部分,切片是可变容量的,其工作原理类似于 Java 的 ArrayList
,当切片容量不足时,便会自动扩容然后返回一个新的切片给我们。
可以使用如下方式声明一个切片:
s := make([]string, 3)
声明了一个长度为 3,容量为 3 的 string
切片。
切片的类型标识看起来和数组很像,但是实际上他们是不同的东西。切片并不需要在 []
内指定一个长度,而数组是需要的。
需要注意的是,切片的 长度(length) 和 容量(capacity) 是两个完全不同的东西,前者才是切片实际的长度,后者则是一个阈值,当切片长度达到该阈值时才会对切片进行扩容。
当然,也可以直接指定一个切片的长度和容量:
s2 := make([]string, 0, 10)
创建了一个长度为 0 ,容量为 10 的 string
切片。
可以直接像数组一样为切片元素赋值:
s[0] = "a"
s[1] = "b"
s[2] = "c"
也可以使用 append
方法为数组添加新的元素:
s = append(s, "d")
s = append(s, "e", "f")
并返回更新后的切片。
可以使用 copy
方法将一个切片内的元素复制到另一个切片中:
c := make([]string, len(s))
copy(c, s)
使用 len
方法获得一个数组,切片的长度。
可以使用和数组相同的方式从切片中获得一个值:
fmt.Println(s[5])
但是不同的是,当我们试图越界访问一个切片时,编译器并不会给我们一个错误(因为切片的长度是不确定的),然而,这会得到一个 panic
,并使程序直接结束运行:
fmt.Println(s[6]) // panic: runtime error: index out of range [6] with length 6
可以使用以下切片操作从数组和切片中截取元素:
fmt.Println(s[2:5]) // [c d e]
将返回一个新的切片,该切片的元素是 s
切片的第 2 个元素到第 4 个值(左闭右开)。
注意,在这种切片操作中,:
左边和右边的数字均可被省略,也就是说:
fmt.Println(s[:5]) // [a b c d e]
将返回切片第 0 个元素到第 4 个元素的切片。
fmt.Println(s[2:]) // [c d e f]
将返回切片第 2 个元素到最后一个元素的切片。
fmt.Println(s[:]) // [a b c d e f]
将返回切片的整个切片(副本)。
映射(Map
)是一个无序 1 对 1 键值对。可以使用如下方式声明一个 Map:
m := make(map[string]int)
声明了一个键(key
)为 string
类型,值(value
)为 int
类型的 Map。
当然,也可以提前初始化 Map 内的值:
m2 := map[string]int{"one" : 1, "two" : 2}
可以使用类似于数组和切片的赋值语法为 Map 赋值,只不过,将索引换成了 key
,目标值换为了 value
:
m["one"] = 1
m["two"] = 2
使用 len
方法获得一个 Map 内包含键值对的长度。
fmt.Println(len(m)) // 2
可以使用和数组和切片类似的方式从切片中获得一个值,只不过,将索引换成了 key
:
fmt.Println(m["one"]) // 1
但实际上,这种写法是非常不好的,因为,当我们试图访问一个不存在的 key
,那么 Map 会给我们返回一个初始值:
fmt.Println(m["unknown"]) // 0, wtf?
因此,我们需要接收第二个值 —— 一个布尔值,来判断该键是否在 Map
中存在:
r, ok := m["unknown"]
fmt.Println(r, ok) // 0 false
最后,使用 delete
函数从一个 Map 中移除指定的键:
delete(m, "one")
可以通过这种语法声明一个带参有返回值函数:
func add(a int, b int) int {
return a + b
}
声明了一个名为 add
,拥有两个类型为 int
,名称分别为 a
和 b
的形参,返回值为 int
的函数。
如果不需要返回值,则可以直接省略,就像 main
函数那样:
func main() {
// ...
}
Go 语言支持指针操作,但默认情况下(不考虑 unsafe
),指针必须指向一个合法对象,而不是一个可能不存在的内存地址,你也不能使用指针进行地址运算(因此,与其说指针,不如称之为引用更加合适):
func add2(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
func main() {
n := 5
add2(n) // not working
fmt.Println(n) // 5
add2ptr(&n)
fmt.Println(n) // 7
}
使用 *type
声明一个指针变量,使用 *
对一个变量进行解引用,使用 &
获取一个变量的指针(引用)。
支持指针的 Go 也侧面印证了,默认情况下,Go 的方法传参均为传值,而不是传引用,如果不传入指针而直接传入一个值的话,则方法实参会被复制一份再传入。
Go 不是一门面向对象(OO)的语言,因此,Go 并没有类(Class)或是其他类似概念,取而代之的,是同类语言中均拥有的结构体(Struct)。
使用如下方式来声明一个结构体:
type user struct {
name string
password string
}
然后,使用如下方式初始化一个结构体:
a := user{name: "wang", password: "1024"}
fmt.Printf("%+v\n", a) // {name:wang password:1024}
如果未对一个结构体进行初始化,则结构体成员将采用默认值:
var b user
fmt.Printf("%+v\n", b) // {name: password:}
可以使用 .
来访问结构体成员
fmt.Println(a.name) // wang
fmt.Println(a.password) // 1024
如果将函数类比为 Java 中的静态方法,那么结构体方法则可以类比为 Java 中的非静态方法(类成员函数)。
使用如下方式声明一个用于检查用户密码是否匹配的方法:
func (u user) checkPassword(password string) bool {
return u.password == password
}
使用如下方式声明一个用于重置用户密码为指定值的方法(注意此处结构体是一个指针,只有这样才可以避免值拷贝,修改原结构体):
func (u *user) resetPassword(password string) {
u.password = password
}
然后即可直接调用:
a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true
与 Java 不同,Go 语言并不支持 throw
,try-catch
这样的操作,与 Rust 比较类似,Go 通过跟随返回值返回返回错误对象来代表方法执行中是否出现了错误 —— 如果返回的值错误对象为 nil
,则代表没有发生错误,函数正常执行。
但是,由于 Go 并没有 Rust 那么强大的模式识别,因此,其错误处理并不能像 Rust 那样便捷有效,并时常饱受诟病(经典的if err != nil
)
以下方法试图从一个 user
切片中查找是否存在指定名称的 user
,如果存在,则返回其指针,否则,返回一个错误。
要实现此功能,需要导入 errors
包:
import (
"errors"
)
声明函数:
func findUser(users []user, name string) (v *user, err error){
for _,u := range users {
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
findUser
函数返回了多个值,这样,我们便可以创建两个变量直接接收它们(类似于 ES6 或 Kotlin 的 解构赋值
语法)。
调用函数:
func main(){
u, err := findUser([]user{{"wang", "1024"}}, "wang")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name) // wang
if u, err := findUser([]user{{"wang", 1024}}, "li"); err != nil {
fmt.Println(err) // not found
return
} else {
fmt.Println(u.name)
}
}
当函数执行完毕后,我们便可通过判断 err
是否为 nil
来得知错误是否发生,然后进行下一步操作。
与 Java 相同,Go 拥有一个非常强大的标准库,包含了字符串操作,字符串格式化,日期与时间处理,JSON 解析,数字解析,进程信息等功能,此处略过不提。
值得一提的是,对于日期和时间处理,Go 使用 2006-01-02 15:04:05
来表达日期和时间模板,而不是传统的 yyyy-MM-dd HH:mm:ss
。
在这一部分,字节内部课:Go 语言上手 – 基础语法通过三个简单的小项目带领学生学习了 Go 语言语法及其标准库使用:一个经典的猜数字游戏,给定一个随机数,让用户猜测这个数并给出与这个数相比是大了还是小了;一个在线词典,通过 HTTP 爬虫爬取其他在线词典网站的结果并返回;一个 SOCKS5 代理,简单的实现了 SOCKS 5 的握手流程,并给予回答。
该文章部分内容来自于以下课程或网页:
This work is licensed under CC BY-SA 4.0