在软件工程领域,有大量的编程语言可供学习,这些语言既有低级语言,也有高级语言。我尝试过其中几种语言,而Golang这种语言似乎同时具备高级语言和低级语言的特点——它虽然属于高级语言,但其性能却非常出色,几乎接近低级语言的水平。Golang是一种运行速度快的静态类型编程语言,类型在编译时就被声明了,因此你可以在运行代码之前就发现其中存在的错误。此外,它还是一种通用型语言,可以用于后端开发、云计算、服务器等领域。
Golang内置了测试支持功能,因此你无需额外安装任何测试库。虽然Golang具有一些面向对象语言的特性,但这些特性是以它自己的方式实现的。它借鉴了一些面向对象的概念,同时也使用了接口、结构体等编程元素。
在本次教程中,我们将介绍学习任何编程语言时都需要掌握的一些基本概念。其中一些概念是许多编程语言共有的,而另一些则是Golang特有的。我们会重点讨论以下内容:
– 变量
– 字符串格式化
– 数组与切片
– 循环结构
– 函数
– 映射
– 结构体
– 包的作用域
在本文结束时,你将会掌握Golang的基本知识,我们还会通过一些示例来了解这些概念在命令行环境中的具体应用方式。
**我们将涵盖的内容:**
– 先决条件
– 如何安装Golang
– 如何编写你的第一个Golang程序
– 如何在Golang中操作变量和数值
– 如何在Golang中格式化字符串
– 如何在Golang中使用数组与切片
– 如何在Golang中使用循环结构
– 如何在Golang中使用函数
– 如何在Golang中使用映射
– 如何在Golang中操作结构体
– Golang中的包作用域
– 结论
**先决条件:**
为了能够顺利跟随本教程学习,你需要掌握任何一种编程语言的基本知识,比如变量、数据类型以及数据结构。我假设你之前至少接触过一种编程语言。
如何安装Go语言
要安装Go语言,请访问golang.org。根据您使用的操作系统,文档会提供不同的安装方法。
我使用的是Ubuntu系统上的WSL(Windows Subsystem for Linux),因此我将在该环境中安装Go语言。
首先,更新您的软件包列表:
sudo apt update
sudo apt upgrade -y
接下来,安装一个用于从互联网下载文件的工具。我们将使用wget:
sudo apt install -y wget
现在下载Go语言的二进制包:
wget https://dl.google.com/go/go1.24.2.linux-amd64.tar.gz
将解压后的文件放置在/usr/local目录下:
sudo tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz
安装完Go语言后,需要将它的二进制文件路径添加到PATH环境变量中,这样在终端中就可以使用go命令了:
export PATH=$PATH:/usr/local/go/bin
您可以通过将上述代码添加到~/.bashrc或~/.profile文件中,使这一设置永久生效。
为了确认Go语言已经成功安装,可以运行以下命令:
go version
终端会显示已安装的Go语言版本信息。
如果您使用VS Code,还可以安装Go扩展程序,以便获得语法高亮功能。
如何编写您的第一个Go程序
在开始编写第一个程序之前,我们需要了解Go语言是如何组织代码的。在Go语言中,每个文件或代码片段都属于某个包。
在这个例子中,我们将创建一个名为main.go的文件。这个文件名在Go语言中并没有特殊含义,但它是用来表示包含程序入口点的文件的常见约定。
package main
import "fmt"
func main() {
fmt.Println("Hello, ninjas")
}
这是我们项目中最重要的文件。我们会将这个文件中的代码纳入一个名为main的包中。
fmt是Go标准库中的一个包,用于格式化字符串并输出到控制台。Println函数的首字母是大写的,因为在这个包中,该方法被声明为可导出的。在Go语言中,被导出的变量或方法的首字母都应该大写。Println的作用是在控制台上打印一行文本。
main函数是Go程序的入口点。一个编译好的Go程序必须恰好包含一个位于package main包内的main函数。(虽然较大的代码库中可以包含多个程序,但每个程序仍然应该放在自己的目录中,并且拥有自己的package main。)
当你运行一个Go程序时,Go工具链会将同一个包中的所有文件编译在一起,然后从main()函数开始执行。文件名main.go并不会决定执行的顺序——它仅仅是一种常见的命名规范而已。
与其他用于将应用程序逻辑打包成库或可重用代码的自定义包不同,main包的作用是用来表明一个程序是一个独立的可执行文件。
要运行这段代码,你只需输入go run main.go,这样就指定了我们要执行的Go文件。
Hello, ninjas
如何在Go中使用变量和数字
如何声明变量
变量其实就是用来存储数据的。Go不允许存在未被使用的局部变量。如果你在函数内部声明了一个变量却从未使用它,编译器会报错。这种机制有助于避免代码中出现冗余或无用的定义。
让我们来看一下如何在Go中声明变量,以字符串为例进行说明。
package main
import "fmt"
func main() {
// 字符串
var nameOne string = "emy"
fmt.Println(nameOne)
}
由于Go是一种静态类型语言,因此每个变量在编译时都必须明确其类型。在这里,我们定义了一个变量nameOne,指定了它的类型,并给它赋了一个初始值。
但如果我们不想自己在编译时指定变量的类型呢?幸运的是,Go允许我们在不指定类型的情况下声明变量。
// 字符串
var nameOne string = "emy"
var nameTwo string = "blessing"
var nameThree string
对于nameTwo,我们不需要明确指定其类型,因为Go会自动推断出它的类型。如果你将鼠标悬停在变量上,就会看到Go已经显示出了该变量的类型。而第三个变量nameThree目前还没有被赋予任何值,因为我们只是声明了它而已。
如果将输出结果打印到控制台,你会看到nameOne和nameTwo被显示出来,但nameThree则不会显示。其实它也被记录在了输出结果中,只是因为它的值还没有被赋定而已。
emy blessing
在Go中还有更简洁的方式来声明变量:
nameFour := "peaches"
在这里,Go也会根据变量被赋予的值来自动推断其类型。注意看,我们甚至没有使用var关键字。这种方法可以在任何函数中使用,并不仅仅限于main函数。不过需要记住的是,当需要对变量进行重新赋值或更新、声明常量,或者声明某些特殊类型的变量时,就不能使用这种声明方式了。
如何声明整数
在声明整数的时候,我们基本上也是使用与上面相同的方法:
var ageOne int = 20
var ageTwo int = 30
ageThree := 40
fmt.Println(ageOne, ageTwo, ageThree)
在声明整数时,我们可以指定该整数所需占用的内存大小或位长度。我们可以将整数声明为int8、int16或int64。
这些不同的内存大小各自能够存储特定范围的数值。例如,int8只能存储-128到127之间的整数,超过这个范围的值会导致错误:
// 位长度与内存
var numOne int8 = 25
var numTwo int8 = -128
var numThree int8 = 129 // 会引发错误
Go语言中还有一种整数类型,即无符号整数,用uint来声明。这种类型只能用于存储正整数,无法用来表示负数。
uint与int一样,也可以搭配不同的位长度进行使用。例如有uint8、uint16等等,但它们能存储的数值范围是不同的。关于整数类型的各种位长度及其对应的数值范围,可以查看这个Go官方文档。
int专门用于表示整数。而如果要表示小数,应该使用float类型。与int类似,float>也有不同的位长度版本,但只有float32和float64>这两种类型,其中float64>能存储比float32>更大的数值。
如何在Go语言中格式化字符串
将字符串输出到控制台
在文章的开头我们就提到了fmt包。下面我们来了解一下这个包提供的一些方法。
首先是有Print()方法,它用于将指定的内容输出到控制台。当我们只需要查看输出结果,而不关心其格式可读性时,就可以使用这个方法。
假设我们有如下两个简单的字符串:
// Print函数的使用
fmt.Print("Hello, ")
fmt.Print("world!)
// 输出结果:Hello, world!
可以看到,上面的代码并没有分成多行显示,而这正是Print方法的缺点。如果我们需要输出多条信息,使用这个方法会导致所有内容被连在一起显示,从而影响可读性。
不过我们可以通过在字符串中添加转义字符\\n来强制换行:
fmt.Print("hello! \\n")
fmt.Print("new line \\n")
/*
hello!
new line
*/
如果在代码中频繁使用转义字符,会显得很繁琐。不过Go语言提供了一个更简便的方法——Println,使用这个方法就可以自动实现换行效果,我们完全不需要手动添加转义字符。
fmt.Println("Hello, friends.")
fmt.Println("How are you?")
/*
Hello, friends.
How are you?
*/
格式说明符
有时,你可能想要将某个字符串输出到控制台,但同时也希望在该字符串中包含一些变量。这种字符串被称为格式化字符串,而我们可以借助格式说明符来实现这一目标。这些格式说明符是Go语言在运行时使用的特殊字符,它们用于确定变量在字符串中的位置。
让我们通过一个具体的例子来更好地理解这个概念:
name := "Emy"
age := 27
fmt.Printf("我的年龄是 %v,我的名字是 %v", age, name)
// 输出结果:我的年龄是 Emy,我的名字是 27
格式说明符%v就是用于表示变量的默认格式说明符。我们可以看到,在这个示例中,Go语言会根据我们传递给Printf函数的参数值(即age和name),在字符串中确定这些变量应该出现在什么位置。需要注意的是,传递参数的顺序是非常重要的。
正如上面所展示的,这段代码的输出结果会是:
name := "Emy"
age := 27
fmt.Printf("我的年龄是 %v,我的名字是 %v", name, age)
你可能还会注意到一个新点:在这段代码中,我们并没有像以前那样使用Print或Println函数。当需要使用格式化字符串功能时,Go语言提供了Printf、Sprintf和Appendf这三个函数供我们选择。
-
Printf函数会将输出结果直接显示到控制台。 -
而使用
Sprintf时,我们可以将输出结果存储在一个变量中,之后再在代码的其他地方使用这个变量。 -
当使用
Appendf时,情况会稍微复杂一些:它会根据格式说明符来生成相应的字符串,并将结果追加到一个字节切片中。不过,由于字节在本文的讨论范围之外,所以我们这里不再详细探讨这个知识点。
除了%v这种格式说明符外,如果我们希望嵌入的变量周围带有引号,也可以使用%q说明符。
name := "Emy"
fmt.Printf("我的名字是 %q", name)
// 输出结果:我的名字是 "Emy"
这种格式说明符适用于name变量,但不适合整数类型的age变量。
name := "Emy"
age := 27
fmt.Printf("我的年龄是 %q,我的名字是 %q", name, age)
// 输出结果:我的年龄是 '\\\\x1b',我的名字是 "Emy"
我们还有一种%T格式说明符,它用于输出变量的类型。
如果我们想要获取age变量的类型,就可以使用这个说明符:
fmt.Printf("变量%v的类型是 %T", age)
这样运行后,会得到如下输出结果:
这是一个类型为int的变量
你可以在官方的fmt包页面上查看其他格式说明符。
如何在Go中使用数组和切片
Go中的数组
在Go中,数组这个概念有点复杂。让我们来看看如何定义一个数组:
var ages = [3]int{20, 25, 30}
在这段代码中,左边是变量名,这是常见的写法。右边部分,[3]表示数组的大小,int表示数组的类型,而中括号内的内容则是数组的值。
一旦声明了数组,我们就无法更改它的大小。老实说,这有点麻烦,因为在编程过程中,我们在声明数组时并不总是知道它到底需要多少个元素(关于这一点,我们后面在讨论切片时会再详细说明)。
此外,Go中的数组也不能包含多种类型的元素。例如,上面声明的数组就不能同时包含字符串类型的数据。
如果我们使用fmt.Println(ages, len(ages))将数组及其长度输出到控制台,那么会得到如下结果:
[20 25 30] 3
Go中的切片
如果你需要在声明数组时不知道或不想指定其大小,就可以使用切片。切片其实是数组的抽象形式,由于它们具有动态大小调整的功能,因此比数组更加灵活。
var scores = []int{100, 50, 60} // 我们没有指定切片的长度
scores[2] = 25 // 修改切片中的某个元素值
scores = append(scores, 50) // 向切片中添加新的元素,此时scores会变成一个新的切片,长度为4
fmt.Println(scores, len(scores)) // [100 50 60 50] 4
在处理数组、切片或任何用于存储数据的结构体时,知道如何获取我们需要的元素是非常重要的。有时候,我们可能只想要获取某个特定范围内的元素,或者根据某些条件来获取位于特定位置的元素等等。
当需要处理一系列元素时,比如说你想输出从索引1(也就是索引0,即第一个元素)到索引3的元素,你可以这样写:
rangeOne := scores[0:3] // [100 50 60]
这里的范围scores[0:3]表示应该列出从索引0到索引3减1之间的所有元素,因此不会包括索引3这个元素。
如果你想输出从索引2开始的所有元素,就可以写scores[2:]。同样地,如果你想输出从切片开头开始直到某个特定位置之前的所有元素,比如索引3之前的元素,就可以写scores[:3]。
如何在Go中使用循环
如何进行循环遍历
Go中的循环与其他编程语言中的循环类似。不过不同的是,Go更侧重于for循环,而对while、do-while或for-each循环则没有过多介绍。
x := 0
for x < 5 {
fmt.Println("x的值为", x)
x++
}
上面的循环非常简单,它只是依次输出从0到4的x的值。
如果要使用迭代器来创建循环,可以这样写:
for i := 0; i < 5; i++ {
fmt.Println("i的值为", i)
}
/*
i的值为 0
i的值为 1
i的值为 2
i的值为 3
i的值为 4
*/
这个循环与上面的循环功能差不多,只不过我们在这里明确指定了迭代器及其范围,并在同一个表达式中完成了遍历操作。
那么,如果想要遍历一个切片呢?同样可以使用迭代器来实现这一目标。
names := []string{"emy", "ble", "winkii"}
for i := 0; i < len(names); i++ { fmt.Println(names[i]) }
如何使用range关键字
与迭代相关的另一个有趣的关键字是range。通过使用range关键字,可以利用循环对切片中的元素进行操作。range会提供切片中元素的索引和值,从而让你在循环中访问这些元素。
for index, value := range names {
fmt.Printf("元素%v的索引为%v \\\\n", value, index)
}
但如果你只需要元素的值而不需要索引呢?其实range要求你必须同时指定索引和值。但是,如果你这样改写循环:
for index, value := range names {
fmt.Printf("值为%v \\\\n", value)
}
Go会抛出错误,因为你在声明了index却并没有使用它。
幸运的是,有一种方法可以绕过这个限制,Go通过使用空标识符_来实现这一点。当某个方法或函数要求你指定一个实际上并不需要的返回值时,就可以使用这个标识符。
for _, value := range names {
fmt.Printf("值为%v \\\\n", value)
}
如果你只需要索引而不需要值,也可以采用同样的方法。
for index, _ := range names {
fmt.Printf("索引为%v \\\\n", index)
}
如何在Go中使用函数
函数是一段可以重复使用的代码。在Go中,函数通常是在main函数之外创建的。这样,其他文件就可以访问并使用这些函数。
package main
import (
"fmt"
)
func sayGreeting(n string){
fmt.Printf("早上好")
}
func main() {
sayGreeting("emy")
}
Go还允许你将一个函数作为参数传递给另一个函数。
func cycleNames(n []string, f func(string)) {上述函数接受一个切片以及一个函数作为参数。而作为参数传递的那个函数本身也会接收一个字符串作为输入。你可以将一个包含名称的切片与问候函数一起传递给这个函数,这样对于切片中的每一个名称,该函数都会被执行一次,从而为相应的名称输出问候语。
for _, value := range n {
f(value)
}
}
func main(){
cycleNames([]string{"emy", "pearl"}, sayGreeting)
}
当将sayGreeting函数作为参数传递时,你并不会立即执行它,因为这个操作已经在cycleNames函数内部完成了。你只是传递了它的引用而已。
具有return值的函数
函数也可能需要返回某个值。那么,Go语言是如何处理这种情况的呢?让我们来看一个简单的例子:
package main
import "fmt"
func sayHello(name string) string {
fmt.Printf("Hello %v", name)
return name
}
func main() {
sayHello("Emy")
}
// 输出:Hello Emy
就像变量一样,我们必须为每个函数参数指定数据类型,同时也需要指定预期的返回值的数据类型。在上面的例子中,sayHello函数接收一个字符串作为参数,并返回另一个字符串。
函数也可以返回多个值,如下例所示:
package main
import "fmt"
func sayHello(name string, age int) (string, int) {
fmt.Printf("Hello %v, you are %v years old", name, age)
return name, age
}
func main() {
sayHello("Emy", 27)
}
与前面的例子一样,我们也需要为函数参数和返回值指定数据类型。
如何在Go语言中使用映射
在Go语言中,映射是一种内置的、无序的键值对集合,类似于Python中的字典或其他语言中的哈希表。映射能够提供快速的查找、插入和删除操作。
使用映射时,所有的键都必须属于相同的数据类型,值也同样如此。如果其中一个键是字符串,那么其他所有键也必须是字符串;值的规则也是如此。
scores := map[string]float64{
"maths": 20,
"english": 15,
"french": 14,
"spanish": 12,
}
fmt.Println(scores)
fmt.Println(scores["maths"])
/*
mapa[english:15 french:14 maths:20 spanish:12]
20
*/
你也可以遍历映射,获取其中的键及其对应的值:
// 遍历映射
for key, value := range scores {
fmt.Println(key, "-", value)
}
/*
french - 14
spanish - 12
maths - 20
english - 15
*/
当涉及到修改映射中的数据时,需要注意的是,映射属于引用类型。引用类型的变量并不存储实际的数据,而是存储指向实际数据的内部指针。这意味着,如果将相同的数据赋值给多个变量,那么当其中一个变量的值被修改时,其他所有包含该相同数据的变量也会被相应地修改。
我们通过一个例子来理解这一点:
package main
import "fmt"
func main() {
scores := map[string]float64{
"maths": 20,
"english": 15,
"french": 14,
"spanish": 12,
}
scores2 := scores
scores2["maths"] = 15
fmt.Println(scores)
}
// 输出:map[english:15 french:14 maths:15 spanish:12]
如果你查看输出结果,就会发现我们修改了scores2中的数学分数,但这种修改同时也影响了原始的映射scores。
如何在Go语言中使用结构体
使用映射的一个很大缺点是,我们只能为键指定一种数据类型,为值也只允许指定一种数据类型。这样的限制确实令人感到不便。我们需要一种能够存储多种不同类型数据的数据结构,而这时结构体就派上了用场。
让我们来看一个例子:
type Book struct {
ID uint
.Title string
Author string
Year int
>UserID uint
}
在这里,我们定义了一个Book结构体。定义结构体时,需要指定该结构体应包含哪些数据以及这些数据的类型。
package main
import "fmt"
type Book struct {
ID uint
_TITLE string
Author string
Year int
>UserID uint
}
func main() {
var book1 Book
book1 = Book{1, "Jane Eyre", "Jane Austen", 1990, 6}
fmt.Println(book1)
}
我们根据结构体的定义提供了相应的数据来初始化它。如果你查看输出结果(当然,可以忽略“Jane Austen并没有写《Jane Eyre》”这一事实):
仔细观察就会发现,结构体与面向对象语言中的类非常相似。它们都可以定义属性(方法也是如此,以大写字母开头的属性是公共的,可以被外部访问),而这些属性可以在方法或函数中被使用。不过两者之间的区别在于继承机制:与类不同,结构体只支持组合关系,而不支持继承。
我们还可以使用点运算符来单独修改结构体的属性,例如:
var book1 Book
book1 = Book{1, "Jane Eyre", "Jane Austen", 1990, 6}
book1.Title = "Things Fall Apart"
book1.Author = "Chinua Achebe"
fmt.Println(book1)
如果你再次查看输出结果,就会发现Title和Author这两个属性的值已经被修改了。
在开发实际系统时,当需要创建模型或设计数据库中要存储的数据结构时,结构体会显得非常有用。
Go语言中的包作用域
在同一包内导入函数
在编写实际应用程序时,我们很少会将所有代码文件都放在同一个文件中。这样做很快就会使代码变得混乱不堪。通常情况下,我们会将某个函数写在一个文件中,然后在另一个文件中调用它。
在这种情况下,你可以在其他文件中声明变量和函数,然后在主程序文件中使用它们。比如说,如果你创建了一个名为greetings.go的文件,并在其中定义了一些变量和方法……
// greetings.go
package main
import "fmt"
var points = []int{20, 90, 100, 45, 70}
func sayHello(n string) {
fmt.Println("Hello", n)
}
请注意,这个文件中并没有func main()函数(记住,在整个应用程序中我们只能有一个main函数)。
现在,在你的main.go文件中,你可以引用你创建的sayHello()函数和points变量。
// main.go
func main() {
sayHello("emy")
for _, v := range points {
fmt.Println(v)
}
}
要运行这段代码,你需要同时执行这两个文件。具体操作如下:
go run main.go greetings.go
你可以看到,输出结果就是调用sayHello()函数并遍历points切片后得到的。
你也可以在main.go文件中定义函数和变量,然后将其传递给greetings.go文件。需要注意的是,所有在这些文件之间传递的函数和变量都必须是在main()函数之外声明的。
在不同包之间导入函数
如果你的应用程序包含100个文件,你会同时运行所有这些文件吗?不会的。由于Go代码可以被组织成不同的包,我们可以从一个包中导入函数到另一个包中使用。让我们用两个不同的包来重写上面的代码。
在greetings.go文件中,我们将代码放在了一个新的包greetings中,并且将SayHello函数的名称首字母大写,这样其他文件就可以导入并使用它了。
// greetings.go
package greetings
import "fmt"
var Points = []int{20, 90, 100, 45, 70}
func SayHello(n string) {
fmt.Println("Hello", n)
}
在main.go文件中,
// main.go
package main
import (
"fmt"
"greetings"
)
func main() {
greetings.SayHello("emy")
for _, v := range points {
fmt.Println(v)
}
}
如你所见,我们导入了在另一个文件中定义的greetings包,并且可以通过点符号访问其中的SayHello()函数。
总结
通过这个教程,你学习了一些Go语言的关键概念。现在你应该已经了解了变量、字符串、数组与切片、循环、函数、映射、结构体以及包的作用域在Go语言中的运作方式。
在这个阶段,你可以尝试自己编写一些更复杂的程序,从而进一步体会Go语言的强大之处。
如果你想更深入地了解这些概念,官方的Go语言教程也是一个非常优秀的资源。