Go语言已经成为开发者社区中最流行的语言之一了。通过在动态语言和静态编译语言之间取得平衡,Go语言在两种类型语言之间提供了一个最佳选择。Go的代码易于理解,规范简洁,并且内置了web服务器。在这篇教程中,我们将不仅介绍Go这门开源语言如此迷人的原因,并且还会介绍其主要的概念。
下面这些是这篇文章将要展开的主题:
- 为什么学习Go?
- Go语言特别的原因
- Hello World:第一个Go程序
- 变量与常量
- 数据类型
- 运算符
- 指针
- Printf函数
- 循环
- 判断分支
- 数组
- Map
- 函数
- 递归
- Defer, Recover, Panic
- 结构体
- 接口
- 实现一个简单的Webserver
在学习一门语言之前,你必须先知道为什么要学习这门语言。每门语言都服务于某个特定领域或场景,Go语言也是如此。那么为什么我们要学习Go语言呢?Go语言最初诞生于Google,其目的是解决开发者面临的一些问题,Google内部有大量的大程序是为在大规模集群上运行的服务器软件而建立的。在Go之前,Google使用C++和Java等语言来解决问题,但这些语言并不具备解决这种规模的问题所需的流畅性和易构建性。在这样的背景下,Ken Thompson和 Robert Greaser开始着手构建一门新的语言以解决这类问题,因此Go语言便诞生了。基本上,如果你想解决一个大规模场景下的问题,或者只是需要高效和可扩展的代码,Go应该是你的 "必选 "语言。
Go语言的特别之处
直到近几年之前,现有的编程语言足用应对大规模编程。但在过去十年中,计算环境发生了很大的变化。这些变化包括大量的网络以及大量的集群计算,也即大众熟知的“云”。到目前为止,用于实现服务程序的语言至少有10到20年的历史,所以现代计算环境的很多属性是这些语言无法直接解决的。因此,拥有一门用于现代计算环境的语言实际上是很重要的。同时,这门语言必须高效,因为它将运行在成百上千台机器上。你也不希望因为有一个低效的解释器或一些通常在虚拟机实现中出现的问题而浪费资源。
Golang满足了所有这些条件,也因此开发者社区获得了当之无愧的声誉。
Hello World:第一个Go程序
package main
import "fmt"
func main () {
fmt.Println("Hello World! This is my first Golang program")
}
我们来一行一行看代码。
第一行是一个包声明。每个go程序必须属于某个包,上面的这个程序属于“main”包。接下来一行,我们引用了“fmt”(或format)库,这个库提供了大量用于输出格式化的函数。下面的一行是创建main函数,这是每个go程序启动后执行的第一个函数。最后是调用Println函数打印输出语句。
变量与常量
变量究竟是什么?变量只是一个程序可操控的存储区域的名称。go语言中的每个变量都有一个特定类型,这个类型决定了分配内存空间的大小,可以赋值的范围,以及能够执行哪些操作。常量指的是一个在程序执行过程中不会变化的固定值。常量可以是任何一种基本数据类型,如整数常量,浮点型常量,字符型常量或是字符串常量。也有一些枚举常量。变量的处理与普通常量一样,只是它们的值在定义后不能修改。
package main
import "fmt"
func main() {
var x int = 5 //声明一个变量
var(
a = 10
b =15 //声明多个变量
)
y := 7 //快捷声明一个变量
const pi float64 = 3.1415926 //声明一个常量
var name string = "Aryya Paul" //声明一个字符串常量
}
数据类型
数据类型 | 值范围 |
uint8 | 0 ~ 255 |
uint16 | 0 ~ 65535 |
uint32 | 0 ~ 4294967295 |
uint64 | 0 ~ 18446744073709551615 |
数据类型 | 值范围 |
int8 | 0 ~ 255 |
int16 | -32768 ~ 32767 |
int32 | -2147483648 ~ 2147483647 |
int64 | -9223372036854775808 ~ 9223372036854775807 |
运算操作符
和大多数主流编程语言一样,Go也有三种类型操作运算符。
指针
接下来看看Go语言中指针的原理。Go中的指针很容易学习,也很有趣。使用指针可以更容易的完成一些编程任务,而一些例如通过引用调用的编程,则必须用到指针。因此想要成为一名优秀的go程序员,学习指针很有必要。众所周知,每个变量代表了一个内存区域,每块内存区域都有它的地址,可以通过符号&来访问,符号&表示内存中的一个地址。
package main
import "fmt"
func main(){
x := 0
changeValue(x) //传值给函数
fmt.Println("x=",x)
changeXValueNow(&x) //将引用传递给函数
fmt.Println("x=",x)
fmt.Println("Memory Address for x =",&x) //获取x对应内存地址
}
func changeValue(x int){
x = 2;
}
//应用传递
func changeXValueNow(x *int){
//修改指针指向地址中保存值的内容
*x = 2 //将值2保存在x指向的内存地址中
}
Printf函数
Printf 在go语言中用于格式化输出,它是format库中的一部分。下表列出Printf函数比较实用的部分。
函数 | 用途 |
fmt.Printf(“%f \n”,pi) | %f 用于浮点型数据 |
fmt.Printf(“%.3f \n”,pi) | 可以设定浮点数的小数位 |
fmt.Printf(“%T \n”,pi) | %T 用于打印数据类型 |
fmt.Printf(“%t \n”,isOver40) | %t 用于打印bool类型数据 |
fmt.Printf(“%d \n”,100) | %d 用于整数型 |
fmt.Printf(“%b \n”,100) | %b 会将100打印数据的二进制形式 |
fmt.Printf(“%c \n”,44) | %c用于字符型 |
fmt.Printf(“%x \n”,17) | %x 用于打印16进制形式 |
fmt.Printf(“%e \n”,pi) | %e 用于打印科学计数类型 |
循环
循环是计算机科学中一个基本的迭代机制,它主要用于在编程中需要执行重复的任务。现在在大多数编程语言中,有三种类型的循环,即for、while(退出控制)和do-while(进入控制),但Go只有一种类型的循环,即 "for "循环。Go的语法通过用 "for "循环的语法来实现while循环。
package main
import "fmt"
func main(){
i := 1
for i < 10 {
fmt.Println(i)
i++
}
for j := 0; j < 10; j++ {
fmt.Println(j)
}
}
条件分支
条件分支是编程的一个重要部分。在Go中,我们可以使用 "if-else "和 "switch "关键字来实现条件分支。让我们看看Golang是如何通过下面这段代码来实现的。
package main
import "fmt"
func main(){
age := 15
//if 条件
if age >= 16 {
fmt.Println("adult")
} else {
fmt.Println("not an adult")
}
// if else-if 分支
if age >= 16 {
fmt.Println("in school")
} else if age >= 18{
fmt.Println("in college")
} else{
fmt.Println("...")
}
//swach 分支
switch age {
case 16: fmt.Println("Go Drive")
case 18: fmt.Println("Go vote")
default: fmt.Println("Go have fun")
}
}
数组
数组是编程中的一种数据结构,可用于存放相同类型的数据。例如,某个班级中所有学生的姓名适合存放在一个字符串数组中。下面的代码展示了数组在go中如何使用:
package main
import "fmt"
func main(){
//定义一个数组变量
var favNums2[5] float64
favNums2[0] = 163
favNums2[0] = 163
favNums2[1] = 78557
favNums2[2] = 691
favNums2[3] = 3.141
favNums2[4] = 1.618
//访问数组
fmt.Println(favNums2[3])
//快速定义并初始化一个数组
favNums3 := [5]float64 {1,2,3,4,5}
//遍历数组
for i , value := range favNums3 {
fmt.Println(value,i)
}
numSlice := []int{5,4,3,2,1}
numSlice2 := numSlice[3:5] //创建一个数组子集numSlice2
fmt.Println("numSlice2[0]",numSlice2[1])
//若不指定起始下标,默认为0
//若不指定结束下标,默认数组最大下标值
fmt.Println("numSlice[:2]",numSlice[:2])
fmt.Println("numSlice[2:]",numSlice[2:])
//定义int数组,初始长度为5,最大长度为10
numSlice3 := make([]int, 5, 10)
//复制数组内容
copy(numSlice3,numSlice)
fmt.Println(numSlice3[0])
//追加元素到数组尾部
numSlice3 = append(numSlice3,0,-1,-2)
fmt.Println(numSlice3[10])
}
Maps
除了数组,还有另一种Map数据结构,它将唯一的键映射到值。键用来检索一个值。给定一个键和一个值,你可以将该值存储在一个Map对象中。在值被存储后,你可以通过使用其键来检索它。
package main
import "fmt"
func main() {
//map通过make函数构造,方式为: varName := make(map[keyType] valueType)
presAge := make(map[string]int)
presAge["Modi"] = 42
fmt.Println(presAge["Modi"])
//获取map元素数量
fmt.Println(len(presAge))
presAge["Gandhi"] = 43
fmt.Println(len(presAge))
//删除map中元素
delete(presAge, "Gandhi")
fmt.Println(len(presAge))
fmt.Println(presAge["Gandhi"])
}
函数
函数是用于实现某个功能的一组语句。每个go程序至少包含一个函数,即main()函数。你可以将代码写入到不同函数中,如何划分取决于你,但通常是每个函数实现一个特定的功能。一个函数声明包含一个函数名称、返回类型、以及入参。
package main
import "fmt"
func main() {
fmt.Println("5+4=", add(5, 4))
fmt.Println(subtract(1, 2, 3, 4, 5))
}
func add(a, b int) int {
return a + b
}
func subtract(args ...int) int {
sub := 0
for _, value := range args {
sub -= value
}
return sub
}
递归
递归是一种重复调用自身的一个过程。如果一个程序允许在函数内部调用自身,那么它就称为“递归函数”调用。go支持递归调用,它允许一个函数调用自己。开发者在使用递归时,需要注意定义函数的退出条件,否则它将成为一个无限循环。
package main
import "fmt"
func main() {
fmt.Println(factorial(5))
}
func factorial(num int) int {
if num == 0 {
return 1
}
return num * factorial(num-1)
}
Defer, Panic, and Recover
defer 会将跟在其后的语句延迟执行,即defer语句会在函数return之后再执行。多个defer语句会被推入堆栈中,以后进先出(LIFO)顺序执行。Defer一般用于关闭资源,如文件、数据库连接等。
panic 类似于其他语言中的抛出异常,通常但调用panic时,正常的执行流程将会中断,但defer语言仍然会正常执行。它是go内置的机制。
recover是另一种go内置的机制,它有助于在panic之后恢复正常执行流程。一般来说,它与defer语句一起使用,已恢复goroutine中的panic异常。
package main
import "fmt"
func main() {
defer printTwo()
printOne()
fmt.Println("Div(3, 0):", Div(3, 0))
fmt.Println("Div(3, 2):", Div(3, 2))
demPanic()
}
func printOne() { fmt.Println(1) }
func printTwo() { fmt.Println(2) }
//如果发生异常,可以通过recover捕获并使程序继续执行
func Div(num1, num2 int) int {
defer func() {
fmt.Println("recover:", recover())
}()
solution := num1 / num2
return solution
}
//panic和recover演示
func demPanic() {
defer func() {
fmt.Println("recover:", recover())
}()
panic("Panic")
}
结构体
go允许定义包含多个相同类型数据项的变量。结构体是go语言中另一种用户可以定义的数据类型,用于将多个不同类型的数据项结合起来。结构体是用来表示一条记录的。假设你想梳理图书馆中的书籍。你可能关心每本书的以下属性:
- Title
- Author
- Subject
- Book ID
在这种场景下,结构体就可以派上用场了。定义一个结构体需要用到type关键字和struct语句。struct语句定义了一种新的数据类型,并提供了多个数据项。type语句会将结构体名称和结构体绑定。
package main
import "fmt"
func main() {
rect1 := Rectangle{height: 10, width: 10}
fmt.Println("Rectangle is", rect1.width, "wide")
fmt.Println("Area of the rectangle =", rect1.area())
}
type Rectangle struct {
height float64
width float64
}
func (rect *Rectangle) area() float64 {
return rect.width * rect.height
}
接口
go编程提供了另外一种叫做接口的数据类型,接口代表了一组方法签名。结构体类型实现这些接口,以拥有接口方法签名的方法定义。
package main
import (
"fmt"
"math"
)
func main() {
rect := Rectangle{20, 50}
circ := Circle{4}
fmt.Println("Rectangle Area = ", getArea(rect))
fmt.Println("Circle Area = ", getArea(circ))
}
// 接口定义了一个类型必须实现的方法列表,
// 如果该类型实现了这些接口,那么就会执行适当的方法,即使原来的方法使用接口名称来指代
type Shape interface {
area() float64
}
type Rectangle struct {
height float64
width float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.height * r.width
}
func (c Circle) area() float64 {
return math.Pi * math.Pow(c.radius, 2)
}
func getArea(shape Shape) float64 {
return shape.area()
}
实现一个简单的Webserver
go还为我们提供了在短时间内建立一个简单的web服务器的能力,这也说明了go语言库的强大。
package main
import (
"fmt"
"net/http"
)
// http.ResponseWriter 将服务器的响应集合起来,并写入到 客户端
// http.Request 代表客户端请求
func handler(w http.ResponseWriter, r *http.Request) {
//写数据到客户端
fmt.Fprintf(w, "Hello world\n")
}
func handler2(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello Earth\n")
}
func main() {
//请求路径为/的请求交给handler函数处理
http.HandleFunc("/", handler)
//请求路径为/earth的请求交给handler2函数处理
http.HandleFunc("/earth", handler2)
//监听服务器的8080端口
http.ListenAndServe(":8080", nil)
}
这篇Go教程到此为止,我希望读者喜欢阅读并有足够的信心去练习Go的基础知识。