前言

大家好,这里是月赎,一个对主流语言都稍微了解一些但基本上都无法称得上掌握的新人开发者

因此,本系列文章将尝试将Go语言与其他常见语言进行对比,以此来体现Go的一些特殊之处与优缺点,希望可以帮助未接触过Go语言的开发者初步了解Go,快速入门Go语言的项目开发。

什么是 Go 语言?

Go 是一门由Google主导开发的高性能,高并发,语法简单,学习曲线平缓的静态类型与强类型语言,这使得它编译之后只需拷贝唯一的可执行文件便可部署运行,部署非常方便快捷,并且每一个变量都有着它自己的类型。与Python相同,其拥有丰富的标准库,完善的工具链,同时相对C++来说,Go的编译速度极快。它的跨平台性能也较为优越,你可以轻松的在你的笔记本上编译出二进制并且拷贝到路由器上进行运行,而无需配置交叉编译环境。与Java类似,Go语言同样有着垃圾回收的设定,这让其开发者可以专注于业务逻辑,而无需考虑内存的分配释放。

Go相比于Python拥有更加强大的性能,还解决了Python的依赖库版本问题,学习成本低廉。同时Go语言拥有着接近C的运行效率和接近PHP的开发效率,以此为根基,在Web工程的领域,Go得以与Java并驾齐驱。

入门 Go 语言

选择 IDE

像其他的语言一样,要想开发 Go 程序,就需要 Go 的开发环境,读者可以前往 Go 官网 根据 安装文档 安装对应平台的 Go 开发环境。

然后,要进行开发,没有 IDE 是不行的。如今有两种主流 IDE 可选:VSCode 和 GoLand。VSCode是由微软开发的开源代码编辑器,通过添加扩展之后可以当做一个功能齐全的 IDE,GoLand 则是由 Jetbrains 公司开发的一款功能强大的 IDE。

GoLand 是一款付费软件,在购买前,你可以进行 30 天的使用,但笔者接下来将会以VSCode为例来简单讲解如何配置 Go 开发环境。

  1. 下载与操作系统适配的Go压缩包
  2. 解压后将地址进行复制
  3. 进入环境变量查看, GOROOT 变量是否存在。没有的话,新增一个 GOROOT 变量,路径则输入你刚刚解压完成后Go的根目录,然后编辑Path变量,新增"%GOROOT%\bin"。
  4. go命令依赖一个环境变量:GOPATH,这不是Go的安装目录,而是你的工作目录(你的代码都会在该目录下),然后编辑Path变量,在最后面新增"%GOPATH%\bin"
  5. 打开命令提示符,然后运行 go version 以确认已安装 Go。
  6. 打开VSCode,添加Go扩展
  7. 这时你可能会收到提示缺少文件,并且点击Install All会出现Install Failed,可以按照教程使用代理之后再次尝试
  8. 不出意外的话在使用代理之后点击Install All后将会安装所有的tools。
  9. 当然,事情往往都不顺利,你可能会在安装dlv时出现your_operating_system_and_architecture_combination_is_not_supported_by_delve报错,打开cmd,键入go env -w GOARCH=amd64即可解决该问题
  10. 一切完成,你兴致冲冲的写出了那个熟悉的Hello,World!但是一个奇怪的报错给你浇了一盆冷水。go: go.mod file not found in current directory or any parent directory。你或许可以回忆起来,在之前我们配置代理的时候,曾经在cmd中键入过go env -w GO111MODULE=on这样一段命令,我们需要再次键入,只不过这次要将on改为auto
  11. 享受你的Go开发之旅吧

学习基础语法

Hello World

package main

import ("fmt") 
func main(){    
	fmt.Println("Hello,World!")
}

以上便是使用 Go 语言输出 Hello World 的代码。不难看出,Go 语言的入口是 main 函数,这点与C++相类似;除此之外,fmt.Println 类似于 cout/printf,可以打印出一段内容。

值得一提的是,Go中不能随意添加换行符,也就是说

package main

import ("fmt") 
func main()
{    
	fmt.Println("Hello,World!")//Error!
}

这样的格式是不被允许的

Go的变量

Go 支持变量类型自动判断,也就是说,当我们可以利用这个特性来缩短工作量:

var a = 1 //int类型

与 C++ 有一定区别,Go 语言的变量类型是后置的,因此你若想声明一个 int 类型的变量你应当:

var a int = 1 //与上面不加int效果一致

与Python类似,允许在同一行将多个值赋值给多个变量:

var b,c int = 1, 2

但是要注意,如果一个变量未初始化(无初始赋值),则必须指定变量类型,此时,变量会被自动初始化:

var d bool

利用前面提到的类型自动判断,我们可以通过 := 符号声明一个变量:

e := 3 // 等价于 var e = 3

如果你想创建一个无法随意更改的常量,可以使用 const 关键字代替 var 关键字:

const f string = "你是?"

Go语言的字符串属于内置类型,可以直接使用“+”进行拼接:

g := f + "我是?"

在Go中,绝大部分运算符的顺序与C++类似,因此笔者在这里便不进行讲解

逻辑语句

对于逻辑语句这一部分,基本上都延续着 C 的语法,因此笔者同样不多进行讲解

选择语句

与 C++ 类似,Go 同样支持 if,else if,else, switch 进行选择控制。

package main

import ("fmt") 
func main(){    
	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")
			}
}

或许你发现,在C++等语言中,if/else 后面会跟上一个小括号,括号内部则是它的判断条件,但是在 Go 中,完全没有必要使用这个括号(因为你就算用了你的编译器也会给你去除的)在某些情况中,你会发现,添加了括号之后,反而会出现报错导致无法正常运行

而与C++不同的另一点,表达式后面的括号绝对不能省略,就算是只有单行语句块,你也不能像其他语言那样直接省略,必须添加括号。

循环语句

在 Go 语言中不存在 for 与 while 的区别。一个经典的 for 语句如下所示:

package main

import ("fmt") 
func main(){
	for j := 7; j < 9; j++ {
		fmt.Println(j)
	}
}

当然,如果您想得到其他语言中 while 语句的效果,将 for 语句中的三段表达式更改成一个布尔值表达式是个不错的选择:

package main

import ("fmt") 
func main(){
	i := 1
	for i <= 6 {
		fmt.Println(i)
		i = i + 1
	}
}

如果不为 for 语句填写任何表达式,除非使用 break 关键字跳出循环,否则这个 for 循环永远也不会停止,这就像 C 中的 while(1):

package main

import ("fmt") 
func main(){
	for {
		fmt.Println("o.O")
	}
}
Switch 语句
a := 3
switch a {
    case 0, 1:
        fmt.Println("0 or 1")
    case 2:
        fmt.Println("2")
    case 3:
        fmt.Println("3")
    default:
        fmt.Println("other")
}

与 C++ 的 Switch 语句类似,但有一点需要注意,在 C++ 中,不使用 break 关键字的话,会遍历完所有的 case 语句,而在 Go 中则不然,在成功匹配之后便不会继续向下遍历

而相比于 C++ 的 Switch 来说,Go 更加强大,支持所有的变量类型,甚至可以作为 if/else 的代替

数组,切片和映射

数组

与变量类似,数组的类型声明也是在后面,于是你可以使用以下方式声明一个指定长度的数组:

var a [3]int
a[2] = 6

上面声明了一个名为 a ,大小为 3 的 int 数组并将其最后一个元素的值设置为 6。

和其他语言一样,Go 数组的索引也是从0开始。

直接使用 := 进行声明当然也可以,但是要注意,数组的类型声明不能省略,即你必须保留 int :

b := [3]int{1, 2, 3}

声明了一个名为 b,大小为 5,数组内元素初始值为 1,2,3 的 int 数组。

可以这样使用索引从数组中取出一个值:

fmt.Println(b[2]) // 值为 3
切片

数组具有固定的长度,所以在实际业务中的使用频率较低,因此,更多情况下我们会使用切片代替数组。

顾名思义,切片(slice)是某个数组或集合的一部分,切片的容量是可变的,与Python的切片类似但又有一些小小的区别,Python中的slice是在原有基础上拷贝一份。Go 中的slice则是指向生成它的数组/切片,且最长长度不会超限。

可以使用如下方式声明一个切片:

s := make([]int, 4)

声明了一个长度为 4,容量为 4 的 int 切片。

相比于数组,切片并不需要在 [] 内指定一个长度,而数组是需要的。

有一点需要留意,切片的 长度 (length) 和 容量 (capacity) 并不可以一概而论,length是切片实际的长度,capacity则是一个阈值,当切片长度达到该阈值时会自动对切片进行扩容。

不过,当然可以直接指定一个切片的长度和容量:

s2 := make([]int, 0, 4)

创建了一个长度为 0 ,容量为 4 的 int 切片。

可以直接像数组一样为切片元素赋值:

s[0] = 0
s[1] = 1
s[2] = 2

和Python一样,可以使用 append 方法为切片添加新的元素,可以使用 copy 方法将一个切片内的元素复制到另一个切片中:

s = append(s, 3)
s = append(s, 4, 5)
a := make([]int, 4)copy(c, s)

从切片中提取值的方法与数组相同:

fmt.Println(s[2])

同样的,类似于Python,我们可以从数组和切片中对元素进行切片操作:

fmt.Println(s[0:2]) // 打印0 1

这样将返回一个新的切片,该切片的元素是 s 切片的第 0 个值到第 1 个值,因为在切片的过程中,遵循左闭右开的原则,同时,左边和右边的数字都是可以省略的,省略之后默认到达切片长度的尽头,也就是说:

fmt.Println(s[:2]) // 打印0 1
fmt.Println(s[2:3]) // 打印2 3
fmt.Println(s[:]) // 打印 0 1 2 3 4 5
映射

Go的Map是一个无序的 1 对 1 键值对,遍历的时候既不会按照字母顺序,也不会按照插入顺序,而是一种随机的顺序。你同样可以使用make来声明一个 Map:

m := make(map[string]int)

上面声明了一个键(key)为 string 类型,值(value)为 int 类型的 Map

对Map来说,你可以选择提前初始化其内部的值,只需要在声明时在类型的后面使用大括号添加key与value

m2 := map[string]int{"one" : 1, "two" : 2}

在后期为Map赋值的方法与数组和切片很相似,只需要将索引换成 key,目标值换为 value:

m["one"] = 1
m["two"] = 2

像切片一样,你也可以使用 len 方法获得一个 Map 内包含键值对的长度:

fmt.Println(len(m)) // 2

同上,你也可以从用切片的方式从Map中取出一个值:

fmt.Println(m["one"]) // 1

你可以使用 delete 函数从一个 Map 中移除指定的键:

delete(m, "one")

不过这种写法有一个致命的缺陷,如果我们试图访问一个不存在的 key,那么 Map 不会抛出异常,而是返回一个初始值,这可能会造成误解:

fmt.Println(m["three"]) //  0

因此,我们需要接收一个布尔值,来判断该键是否在 Map 中存在:

r, ok := m["three"]
fmt.Println(r, ok) // 0 false

Range

和C++类似,Go的range函数可以用来快速遍历slice和map

当我们使用 for range 语句遍历一个数组或切片时,我们将得到该集合元素的索引和对应值:

而当你遍历map的时候,同样会返回两个值,分别是键key和它对应的值value

当你不需要循环中的某个值时,可以使用下划线来进行忽略

// 当你只需要map中的对应值时
for _, v := range m {
}

函数

您可以通过func声明一个带有参数并且有返回值函数:

func add(a int, b int) int {
    return a + b
}

上面声明了一个简单的将两数相加的函数,其函数名为 add,拥有两个类型为 int,名称分别为 a 和 b 的形参,返回值的类型同样为 int 

如果不需要返回值,则可以将return部分省略

Go 里面的函数原生支持返回多个值,所以在真正的业务运用时,往往返回两个值,第一个是真正的返回值,第二个是错误信息

指针

Go 的指针与C++相比,支持的操作非常有限,主要的用途就是对传入的参数进行修改:

func add2(n int) {
    n += 2 // 无效,因为本质上这个n是一个副本
}
 
func add2ptr(n *int) {
    *n += 2 
}
 
func main() {
    n := 5
    add2(n)
    fmt.Println(n) // 5
    add2ptr(&n) // 调用的时候记得加&使类型匹配
    fmt.Println(n) // 7
}

上面这段代码实际上指出,Go 的方法对于参数的传递,实际上是传值,而不是传引用,如果通过指针指向地址而直接传入一个值的话,本质上是传入了这个值的副本。

结构体

Go 受限于自身的预言性质,与C++类似,同样有着结构体的概念。

声明一个结构体的方法如下:

type user struct {
    name     string
    password string
}

然后,可以使用如下方法对结构体进行初始化:

a := user{name: "wang", password: "1024"}

和C++类似,可以使用 . 来访问结构体成员

fmt.Println(a.name) // wang
fmt.Println(a.password) // 1024
结构体方法

声明一个用于检查用户密码是否匹配的方法的方式如下:

func (u user) checkPassword(password string) bool {
    return u.password == password
}

如果你想要更改结构体中的内容,上面的代码就做不到了,因为只有使用指针,才能够修改传入参数

下面是一个重置密码的方法

func (u *user) resetPassword(password string) {
    u.password = password

Go 错误处理

在Go语言中,错误处理符合语言习惯的就是单独使用一个返回值来返回错误信息,不同于 Java 使用的抛出异常的方法,Go语言可以很清晰的知道哪个函数返回了错误,并且可以用简单的if/else来处理

因此,Go 往往在函数的返回值里面加入一个error,而要实现此功能,需要导入 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 函数返回了多个值,正如上面所说,第一个是真正的返回值,第二个是nil,这在Go中代表没有发生错误

字符串操作与格式化

这一部分的方法来自于标准库 strings 包,在这个包内有着许多常用的字符串工具函数,比如contains来判断一个字符串里是否含有另一个字符串,index查找某个字符串的位置等等、

而标准库 fmt 包里有很多的字符串格式相关的方法,较为常用的 printf 这个便类似于 C++ 中的printf函数,不过在Go语言中,你可以使用 %v 来方便的打印各种类型的变量,而不用区分数字和字符串

%+v打印详细结果,%#v更详细

Go 标准库

正如前文所提到,Go 拥有一个非常强大的标准库,包含了众多的功能,除了上面提到的fmt,strings,errors外,还有着encoding/json用来进行json的处理,time进行时间相关处理,最常用的便是time.now()来获取当前时间,strconv进行字符串与数字之间的转换,这里有个有趣的点,strconv这个包名实际上是由string convert两个单词的缩写拼接而成,os,os./exec来获取进程信息等等

结语

在这一部分,通过走进 Go 语言基础语法这段课程的学习,让人大体了解了Go语言的语法规则,为后续的程序开发奠定了基础。


非自律型摸鱼AI