后端小白的Go之旅1|青训营笔记

74 阅读11分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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语言的结构为

  1. 包声明 package main

  2. 引入包

    import ( "fmt" )

    其中fmt是go内置的format包

  3. 主函数 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