这是我参加「第五届青训营」伴学笔记创造活动的第 1 天
前言
本系列文章企图从一名 Java 开发者(有时也会交叉其他言语)的角度窥探 Go 言语,并以注释的办法提及 Go 与 Java 的一些差异,便利 Java 开发者迅速入门 Go 言语。
什么是 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 言语
挑选 IDE
要想开发 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 教育许可证。
学习根底语法
Hello World
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
Go 过错处理
与 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
来得知过错是否发生,然后进行下一步操作。
Go 规范库
与 Java 相同,Go 具有一个非常强壮的规范库,包括了字符串操作,字符串格式化,日期与时刻处理,JSON 解析,数字解析,进程信息等功用,此处略过不提。
值得一提的是,关于日期和时刻处理,Go 运用 2006-01-02 15:04:05
来表达日期和时刻模板,而不是传统的 yyyy-MM-dd HH:mm:ss
。
Go 言语实战
在这一部分,字节内部课:Go 言语上手 – 根底语法经过三个简略的小项目带领学生学习了 Go 言语语法及其规范库运用:一个经典的猜数字游戏,给定一个随机数,让用户猜测这个数并给出与这个数相比是大了仍是小了;一个在线词典,经过 HTTP 爬虫爬取其他在线词典网站的成果并回来;一个 SOCKS5 署理,简略的完成了 SOCKS 5 的握手流程,并给予回答。
引证
该文章部分内容来自于以下课程或网页:
- 字节内部课:Go 言语上手 – 根底语法
- 2 patterns for a do-while loop in Go YourBasic Go
- Go Cheat Sheet & Quick Reference
- Switch golang/go Wiki (github.com)
- Go 言语教程 | 菜鸟教程 (runoob.com)
分发
This work is licensed underCC BY-SA 4.0