这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
本系列文章将从后端小白的视角出发认识go语言,本文的主要内容是go语言的简介和语法。
Go语言简介
Go 是一个开源的、非面向对象的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
Go语言的特色
- 简洁、快速、安全
- 并行、有趣、开源
- 内存管理、数组安全、编译迅速
Go语言的用途
Go 语言被设计成一门应用于搭载 Web 服务器,存储集群或类似用途的巨型中央服务器的系统编程语言。
对于高性能分布式系统领域而言,Go 语言无疑比大多数其它语言有着更高的开发效率。它提供了海量并行的支持,这对于游戏服务端的开发而言是再好不过了。
关于Go语言的环境安装,在此查看
关于Go语言的开发工具,在此查看
Go基础语法
Go语法部分的内容包括以下几点
- 变量声明、初始化部分
- for循环
- if、switch语句
- array和slice的区别和使用方式
- map数据类型(字典)的定义及初始化
- range遍历方法
- 函数的定义
- 指针point的使用
- 结构体struct的定义
- 结构体方法的使用
- Go错误处理
Hello World
package main
import (
"fmt"
)
func main(){
fmt.Println("hello world")
}
此为go的hello world代码,可以看出,go语言的结构为
-
包声明
package main -
引入包
import ( "fmt" )其中fmt是go内置的format包
-
主函数
func main()...
此外,";"作为每句的结束符在go语言中是不必要的,除非一行有多个句子; "{" 符号不能单独起一行,必须跟在语句的后面 ;还有Println中的P是大写,实际上在go语言中函数/方法大写意味着可以被其他包调用。
变量声明、初始化部分
var a = "initial"
var b, c int = 1, 2
var d = true
var e float64 // 初始化为0
与C语言不同,go的变量类型是后置的,并且可以一行声明多个变量
go还支持变量的自动推断,即我们可以在声明变量后忽略类型直接赋值
但是如果声明变量的时候没有赋值,我们必须声明完后手动设置变量的类型
f := 5.1 // 等同于 var f = 5.1
go语言中还可以使用":="对一个未声明对变量进行赋值(比较常用的方式)
const h string = "constant"
除了使用var(variable 变量),还可以使用const(constant 常量)关键字来创建一个常量
循环语句
for j := 7; j < 9; j++ {
fmt.Println(j)
}
//在此初始化j的时候一般使用":="进行赋值
与其他语言区别较大的是, go语言只使用for关键字创建循环
for {
fmt.Println("loop")
break
} // 相当于py里的while(True)
i := 1
for i <= 3{
fmt.Println(i)
i = i + 1
}// 相当于py里的while(i <= 3)
go语言中while语句的表达方式如上
if、switch 语句
在流程控制方面,go与其他语言相似,支持if、else、elseif、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 multiple digits")
}
需要注意的是, go语言中的switch语句比较特殊,其中不需要加上break,默认跑完满足条件的一行后直接跳出switch语句,而若想与C语言中的switch一样则需要在且仅在case的末尾加上 fallthrough 关键字。
a := 2
// a可为字符串、结构体
switch a {
case 1:
fmt.Println("one")
// 不需要加上break,默认跑完满足条件的一行后直接跳出switch语句
case 2:
fmt.Println("two")
case 3:
fmt.Println("three")
case 4, 5:
fmt.Println("four or five")
default:
fmt.Println("other")
}
a := 2
// a可为字符串、结构体
switch a {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
fallthrough
case 3:
fmt.Println("three")
fallthrough
case 4, 5:
fmt.Println("four or five")
fallthrough
default:
fmt.Println("other")
}
array和slice的区别和使用方式
数组array
以下为数组的声明方式,数组的长度是固定的
var a [5]int
a[4] = 100
arr := [5]int{1,2,3,4,5}// 声明了一个名为arr,大小为 5,数组内元素初始值为 1,2,3,4,5 的 int 数组。
var twoD [2][3]int
for i := 0; i < 2; i++ {
for j := 0; j < 3; j++ {
twoD[i][j] = i + j
}
}
切片slice
在实际业务中,由于数组长度是固定的,因此很少使用数组。切片不同于数组,可以认为是一个可变长度的数组,可以在任意时刻更改长度,也有更多丰富的操作
切片的原理:{长度,容量,指向数组的指针}
可以使用make来创建一个切片
s := make([]string, 3) // 创建一个string类型切片,初始长度为3
s[0] = "a"
s[1] = "b"
s[2] = "c"
切片可以使用append进行末尾追加,与py中的.append方法类似
注意,切片的长度和容量是不同的,长度指的是切片当前的长度,而容量指的是一个阈值,当进行append操作的时候,如果当前长度超过了容量,则会发生扩容,返回一个新的slice并赋值回去
s = append(s, "d")// a b c d
s = append(s, "e", "f")// a b c d e f
s1 := make([]string, 0, 10) // 创建一个长度为0,容量为10的切片
同时切片可以使用copy进行拷贝
c := make([]string, len(s)) // 可以使用len方法获得一个数组或切片的长度
copy(c, s)
切片还有像py一样的切片操作
fmt.Println(s[2:5]) // [c d e]
fmt.Println(s[:5]) // [a b c d e]
fmt.Println(s[2:]) // [c d e f]
map数据类型(映射)的定义及初始化
map数据类型类似py里的字典结构,其是一个一对一的键值对
可以使用make声明一个map:
m := make(map[string]int)// 声明了一个键为string类型,值为int类型的映射
m1 := map[string]int{"one":1, "two":2}//若在创建的时候直接赋值,则不需要使用make关键字
使用len可以获得一个map里键值对的个数
fmt.Println(len(m)) // 2
用key作为索引获得对应值
fmt.Println(m["one"]) // 1
但若访问了一个不存在的key,则会返回一个初始值
fmt.Println(m["unknow"]) // 0
因此,需要接收第二个值(布尔类型),来判断是否存在此key值
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
可以使用delete从一个Map中移除指定的键
delete(m, "one")
range遍历方法
可以使用 for range 循环的方式来遍历一个数组,切片,集合,映射
- 使用range遍历数组:使用range得到数组的索引i, 对应值num
nums := []int{2, 3, 4}
sum := 0
for i, num := range nums {
sum += num
if num == 2 {
fmt.Println("index:", i, "num:", num) // index: 0 num: 2
}
}
- 使用range遍历映射:
// 映射的遍历顺序是随机的,每次执行遍历的结果可能不一样
m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
fmt.Println(k, v) // b B; a A
}
for k := range m {
fmt.Println("key", k) // key a; key b
}// 只遍历键
for _, v := range m {
fmt.Println("value", v) // value B; value A
}// 只遍历值
函数的定义
func add(a int, b int) int{
return a + b
}
声明了一个名为add, 名称为a、b类型均为int的形参, 返回值为int的函数
func exists(m map[string]string, k string) (v string, ok bool) {
v, ok = m[k]
return v, ok // 函数原生支持返回多个值,在此句第一个值是返回结果,第二个是错误信息
}
指针point的使用
Go 语言中指针是很容易学习的,Go 语言中使用指针可以更简单的执行一些任务。
我们都知道,变量是一种使用方便的占位符,用于引用计算机内存地址。Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址。
以下实例演示了变量在内存中地址:
package main
import "fmt"
func main() {
var a int = 10
fmt.Printf("变量的地址: %x\n", &a )
}// 变量的地址: 20818a220
一个指针变量指向了一个值的内存地址。
类似于变量和常量,在使用指针前你需要声明指针。指针声明格式如下:
var var_name *var-type
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。以下是有效的指针声明:
var ip *int /* 指向整型*/
var fp *float32 /* 指向浮点型 */
指针使用流程:
- 定义指针变量。
- 为指针变量赋值。
- 访问指针变量中指向地址的值。
func main() {
var a int= 20 /* 声明实际变量 */
var ip *int /* 声明指针变量 */
ip = &a /* 指针变量的存储地址 */
fmt.Printf("a 变量的地址是: %x\n", &a )
/* 指针变量的存储地址 */
fmt.Printf("ip 变量储存的指针地址: %x\n", ip )
/* 使用指针访问值 */
fmt.Printf("*ip 变量的值: %d\n", *ip )
}
//a 变量的地址是: 20818a220
//ip 变量储存的指针地址: 20818a220
//*ip 变量的值: 20
具体使用指针的函数如下:
func add2(n int) {
n += 2
}
func add2ptr(n *int) {
*n += 2
}
// golang初步支持指针 主要用途是对传入的参数进行修改
func main() {
n := 5
add2(n)
fmt.Println(n) // 5
add2ptr(&n) //对传入的参数进行修改时要带上"&"符号
fmt.Println(n) // 7
}
为什么要用指针:答:可以节省时间和空间资源。例如,传入函数的参数是一个非常大的数组,那么需要先对此参数进行拷贝,再送入函数中操作,返回的值再重新拷贝回来,而如果使用指针,传入的则是这个数组的内存地址,在函数中直接对此数组进行操作,且不需要返回值,从而能够节省拷贝此大型数组的时间。
Go空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil。
nil 指针也称为空指针。
nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。
一个指针变量通常缩写为 ptr。
结构体struct的定义
用如下方式声明一个结构体:注意结构体中的元素不需要加上var关键字
type user struct{
name string
password string
}
用如下方法来初始化一个结构体:
//结构体声明
var s user
//或者直接使用:=进行声明加初始化
s := user{name:"zhuo", password:"zhuo123"}
fmt.Printf("%+v/n", s)//使用%+v得到详细结构 {name:zhuo password:zhuo123}
var b user
fmt.Printf("%+v\n", b) // {name: password:} 未初始化则采用默认值
使用'.'来访问结构体成员
fmt.Println(s.name)//zhuo
fmt.Println(s.password)//zhuo123
结构体方法的使用
使用如下方式声明一个用于检查用户密码是否匹配的方法:
func (u user) checkPassword(password string) bool{
return u.password == password
}
不同于普通函数的写法,结构体方法中要先指定结构体类型( u user 结构体类型为user, 名称为u的形参),后面跟着普通的函数的写法:函数名+(形参名 类型)+返回类型
使用如下方式声明一个用于重置用户密码的指针方法:
func (u *user) resetPassword(newpassword string){
u.password = newpassword
}
//调用
s.resetPassword("zhuo12")
fmt.Println(s.checkPassword("zhuo12")) // true
使用指针方式可以避免结构体复制造成的性能损耗
Go错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制。
error类型是一个接口类型,这是它的定义:
type error interface {
Error() string
}
我们可以在编码中通过实现 error 接口类型来生成错误信息。
函数通常在最后的返回值中返回错误信息。使用errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("math: square root of negative number")
}
// 实现
}
在下面的例子中,我们在调用Sqrt的时候传递的一个负数,然后就得到了non-nil的error对象,将此对象与nil比较,结果为true,所以fmt.Println(fmt包在处理error时会调用Error方法)被调用,以输出错误,请看下面调用的示例代码:
result, err:= Sqrt(-1)
if err != nil {
fmt.Println(err)
}
实际使用时,需要引入error库,下面是一个遍历结构体数组users中的每个user切片,判断是否存在指定名称的user,有则返回此名称(指向该名称所在的内存地址),没有则返回错误信息:
package main
import (
"errors"
"fmt"
)
type user struct {
name string
password string
}
// 在可能返回错误值的函数的返回值内加上error类型的变量
func findUser(users [2]user, name string) (v *user, err error) {
for _, u := range users { //"_"是此过程中不需要使用的索引,"u"是当前结构体
if u.name == name {
return &u, nil
}
}
return nil, errors.New("not found")
}
func main() {
var s, n user //定义结构体
var sArr [2]user//定义结构体数组
s.name = "zhuo"
s.password = "zhuo123"
n.name = "x"
n.password = "x123"
sArr[0] = s
sArr[1] = n
u, err := findUser(sArr, "zhuo")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name)
u, err = findUser(sArr, "li")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(u.name)
}
//zhuo
//not found