menu Chancel's blog
rss_feed
Chancel's blog
时来天地皆同力

Golang

作者:Chancel Yang, 更新:2023 Jan 06, 字数:10465, 已阅:13

Go语言常应用于网络编程、系统编程、并发编程、分布式编程中,比如常见的Docker、Kubernetes、Thrraform都是基于Golang语言,GO是一门编译型语言且自带编译器,兼具快速编译(交叉编译)以及高效性能,作为一款编程语言,其特性包括了简洁语法、并发支持(Goroutine)、内存分配(tcmalloc)、标准库以及完整的工具链。

1. 开发环境

Go的开发环境部署较为简单,IDE方面首推Goland/Visual Studio Code

本次搭建基于Ubuntu2204,其他操作系统环境可参考步骤

1.1. Go环境

创建golang的安装文件夹

mkdir -p $HOME/apps/golang && cd $HOME/apps/golang
wget https://go.dev/dl/go1.18.1.linux-amd64.tar.gz 
tar -zxvf https://go.dev/dl/go1.18.1.linux-amd64.tar.gz

添加$HOME/apps/golang/go/binPATH

sed -i '$a PATH=$PATH:$HOME/apps/golang/go/bin' ~/.zshrc

检测版本

❯ go version
go version go1.18.1 linux/amd64

1.2. Hello World

创建一个示例项目hello-world

mkdir -p $HOME/codes/golang/hello-world && cd $HOME/codes/golang/hello-world

hello-world项目中创建一个main.go的文件

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

运行main.go

❯ go run main.go
hello world

1.3. 第三方包

沿用hello-world项目,修改main.go引入web框架GIN,输出一个HTML页面的hello world

package main

import (
    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
    	c.String(200, "hello world")
    })
    r.Run()
}

要使用第三方依赖包,需要借助go mod,go版本1.11之后go mod作为包的默认管理工具,它提供了对于包的依赖管理、部署构建

Go mod provides access to operations on modules.

在当下的go版本中,go mod是默认开启的,可以使用go env来检查,

# GO111MODULE有3个值,默认是auto,on/off指示开关
❯ go env | grep MODULE
GO111MODULE=""

如无特殊项目要求,保持默认(auto)即可,此时go会自动判断项目类型并查找依赖

go mod的常用语法如下

go mod <command> [arguments]

The commands are:

        download    download modules to local cache
        edit        edit go.mod from tools or scripts
        graph       print module requirement graph
        init        initialize new module in current directory
        tidy        add missing and remove unused modules
        vendor      make vendored copy of dependencies
        verify      verify dependencies have expected content
        why         explain why packages or modules are needed

使用go mod初始化包依赖管理,并下载main.go中使用到的web框架

go mod init hello-world
go mod tidy

此时项目结构如下

❯ tree
.
├── go.mod
├── go.sum
└── main.go

go.mod是go modules的配置文件,go.sum记载了依赖包的版本与校验信息

此时我们运行项目,访问http://localhost:8080/即可以看到hello world

1.4. Visual Studio Code

Visual Studio Code对于Golang开发也提供强大的支持,以上述的hello-world项目为例,在终端用vsc打开该项目

code $HOME/codes/golang/hello-world

在扩展插件中搜索go并安装,安装完成后按ctrl+shift+p,输入go install,选择go: Install/Update tools,勾选所有项并点击ok,如下

Peek 2023-01-06 11-11.gif

创建.vscode/launch.json,内容如下

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch Package",
            "type": "go",
            "request": "launch",
            "mode": "auto",
            "program": "${workspaceFolder}/main.go",
            "console": "internalConsole"
        }
    ]
}

按F5可启动单步调试,过程如下所示

Peek 2023-01-06 11-16.gif

2. GO语法

Go对大小写敏感,命名规范原则上遵循对外可见则大写开头(UserModel),对外不可见则是小写开头(userModel)

命名推荐规范如下

类型 规范 例子
包名 小写 net/http
文件名 下划线区分单词 web_api.go
接口/结构体/函数 驼峰命名 WebApi

2.1. 变量

GO的基础类型包含boolstaringint/int8/int16/int32(rune)/int64uint/uint8(byte)/uint16/uint32/uint64float32/float64以及complex64/complex128

float32精确到小数点后7位,float64精确到小数点后64位

因为精确度的问题,使用==和!=比较float类型时应小心

变量声明代码如下

// 单变量声明
var userName string = "chancel"

// 多个变量声明
var (
    userName string = "chancel"
    userAge int = 10
    userHistory = []float32 {0.01,0.2}
    getName func() string
    userFamily struct {
        fatherName string
    }
)

Go语言的变量类型转换比较特殊,大多数时候都借助strconv,如下

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // string to int
    strVar := "100"
    inVar, err := strconv.Atoi(strVar)
    if err != nil {
    	fmt.Println(err)
    }
    fmt.Println(inVar) // 100

    // int to string
    s1 := strconv.FormatInt(int64(inVar), 10)
    fmt.Println(s1) // 100
}

Go语言有匿名变量,通常采用关键字_,该变量不占用内存空间也不分配内存

package main

import (
    "fmt"
)

func getData() (int, int) {
    return 1, 2
}

func main() {
    var a, _ = getData()
    fmt.Println("a value is ", a)
}

2.2. 指针运算符

指针运算符是Go语言被归入C语言家族的重要原因,指针是指向变量值内存地址的变量

一个内存地址用一个操作系统原生字(native word)来存储。 一个原生字在32位操作系统上占4个字节,在64位操作系统上占8个字节

在Go里,一个指针的形式为*T,类型T被成为指针类型的base type,指针通常缩写为Ptr

指针的声明如下,通常采用前面一种(无名指针类型)

*int

type Ptr *int

对于其他编程语言来说,指针是比较晦涩难懂的,举例说明

package main

import "fmt"

func double(x *int) {
    *x += *x
    x = nil 
}

func main() {
    var a = 3
    double(&a)
    fmt.Println(a) // 6
    p := &a
    double(p)
    fmt.Println(a, p == nil) // 12 false
}

Go语言也对指针做了很多限制,举例说明

package main

import "fmt"

func main() {
    a := int64(5)
    p := &a

    // 下面这两行编译不通过。
    /*
    p++
    p = (&a) + 8
    */

    *p++
    fmt.Println(*p, a)   // 6 6
    fmt.Println(p == &a) // true

    *&a++
    *&*&a++
    **&p++
    *&*p++
    fmt.Println(*p, a) // 10 10
}

2.3. 数组

在Go中,数组必须在声明时固定长度且无法动态添加元素的

如下声明一个没有初始值数组 array_1 和拥有初始值的数组 array_2

package main

import "fmt"

func main() {
    var array_1 [5]int
    fmt.Println(array_1) // [0,0,0,0,0]

    var array_2 = [5]int{1, 2, 3, 4, 5}
    fmt.Println(array_2) // [1,2,3,4,5]

}

固定数组的使用场景较少,实际开发更需要可变数组

可变数组的用途非常广泛,在Go中可以借助 Slice(切片) 来实现可变数组

切片的常见使用如下

package main

import (
    "fmt"
    "sort"
)

func main() {
    var array_1 [5]int
    fmt.Println(array_1)

    var array_2 = [5]int{3, 5, 7, 8, 1}
    fmt.Println(array_2)

    var slice_1 = array_2[:]
    fmt.Println(slice_1[1:4]) // [5,7,8]

    fmt.Println("slice length:", len(slice_1), "cap:", cap(slice_1), "ptr:", &slice_1[0]) // slice length: 5 cap: 5 ptr: 0xc0001aa060
    slice_1 = append(slice_1, 4, 10, 2)
    fmt.Println("slice length:", len(slice_1), "cap:", cap(slice_1), "ptr:", &slice_1[0]) // slice length: 8 cap: 10 ptr: 0xc0001ba000
    slice_1 = append(slice_1, 9, 6)
    fmt.Println("slice length:", len(slice_1), "cap:", cap(slice_1), "ptr:", &slice_1[0]) // lice length: 10 cap: 10 ptr: 0xc0001ba000

    // 对元素进行排序
    sort.Ints(slice_1)
    fmt.Println("slice content:", slice_1) // slice content: [1 2 3 4 5 6 7 8 9 10]
}

可以看出每当cap变化的时候,切片的内存地址就会发生改变,说明了cap与length的区别在于cap是空间长度,如添加元素时cap长度不足,则Go会采用以下公式来进行扩容

var newSclice = make(int[],len(oldSlice),2*len(oldSlice)+1)

2.4. map

Go语言的字典是哈希结构的,key的类型必须是可直接比较的类型(如Slice/Func/map等无法作为key值),而Vlaue的类型则没有任何限制

所以借助value类型为slice的字典可实现Python语言的dict类型的效果

常见使用如下

package main

import "fmt"

func main() {
    map_1 := map[string]string{"name": "zhangsan"}
    fmt.Println(map_1) // map[name:zhangsan]

    // 遍历
    for k, v := range map_1 {
    	fmt.Println(k, v)
    }

    // 插值
    map_1["age"] = "18"

    // 查找
    value, exist := map_1["name"]
    if exist {
    	println(value) // zhangsan
    }

    //删除
    delete(map_1, "age")
    fmt.Println(map_1) // map[name:zhangsan]
}

3. GO语法

3.1. 接口

3.1.1. 定义

Go的接口是支持面向对象编程(OOP)的重要标志,与其他语言的接口实现相比,go里面的类型不需要声明实现接口,只要实现了接口的方法即实现了接口

通俗地讲,go的接口是方法/行为的集合,往往作为一种通用类型参数

定义一个求面积的矩形接口,再定义一个计算体积的方法Bulk,分别计算圆与长方形的体积代码如下

package main

import (
    "fmt"
    "math"
)

// 定义接口 shape
type Shape interface {
    Area() float64 //求面积
}

// 定义了结构体 circle
type Circle struct {
    radius float64
}

// 定义结构体 rectangle
type Rectangle struct {
    length float64
    width  float64
}

// 结构体 circle 实现了 shape.area 方法
func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

// 结构体 rectangle 实现了 shape.area 方法
func (r Rectangle) Area() float64 {
    return r.length * r.width
}

// 体积计算
func Bulk(s Shape, h float64) float64 {
    return s.Area() * h
}

func main() {
    var c Circle
    c.radius = 3
    fmt.Println(c.Area()) // 28.274333882308138
    b := Bulk(c, 5)
    fmt.Println(b) // 141.3716694115407

    r := Rectangle{5.0, 8}
    fmt.Println(r.Area()) // 40
    b = Bulk(r, 5)
    fmt.Println(b) // 200
}

可以看到,结构体 CircleRectangle 并不需要声明实现了接口 Shape,但在方法 Bulk中可以将他们看成 Shape 接口类型参数

接口为Go带来了抽象数据的能力,从而实现OOP(Object Oriented Programming)

3.1.2. 接口嵌套

此外,类型不一定需要实现接口的所有方法才实现了接口,可以通过嵌入其他结构体来获得其实现方法,如下

package main

import (
    "fmt"
)

type AirCondition interface {
    heating()
    refrigerating()
}

type Heater struct{}

func (h Heater) heating() {
    fmt.Println("heating...")
}

type Haier struct {
    Heater // 注意此处不是结构体变量,而是将结构体Heater直接嵌入到Haier中
}

func (h Haier) refrigerating() {
    fmt.Println("refrigerating...")
}

func main() {
    h := Haier{}
    h.refrigerating() // heating...
    h.heating() // refrigerating...
}

3.1.3. 接口技巧

此外,在实际开发中常用到空接口类型如var x interface{},使用空接口类型作为形参可以实现任意传参

package main

import (
    "fmt"
)

func println(x interface{}) {
    fmt.Println(x)
}

func main() {
    a := "hello,world"
    b := 100

    println(a) // hello,world
    println(b) // 100
}

判断接口类型与值可以使用interface.(type)来判断,如下

package main

import (
    "fmt"
)

func main() {
    var x interface{}

    a := "hello,world"
    x = a

    v, ok := x.(string)
    if ok {
    	fmt.Println(v)
    } else {
    	fmt.Println("error")
    }
}

3.2. 方法与函数

方法与函数是类似的,区别在于方法是一类带有接受者(也就是实例)的特殊函数

所以在Go语言里面,方法可以看成是带有类型的函数

a Go method is a function that acts onvariable of a certain type, called the receiver. So a method is a specialkindof function ——《The Way to Go》

3.3. defer关键字

defer是延迟调用,执行方式是先进后出,常用于释放资源如关闭文件写入,关闭数据库连接等

与Python中的with语法较为相似

代码例子如下

package main

import (
    "fmt"
)

func main() {
    for i := 0; i < 5; i++ {
    	defer fmt.Println(i) //4 3 2 1 0
    }
}

3.4. Json

在处理Json上,Go采用了标签标识的方法来解决json字段与结构体字段对应的问题

除了标识对应关系外,还可以添加例如omitempty来标识序列化时忽略0值或Nil值

代码如下

package main

import (
    "encoding/json"
    "fmt"
)

type Student struct {
    Name   string `json:"username"`
    Age    int    `json:"age,string"`
    Gender int    `json:"gender"`
    Score  int    `json:"score,omitempty"`
}

func main() {
    var data string = `{ "username":"chancel", "age":"18", "gender":0, "Score":666 }`
    var s = &Student{}
    var err = json.Unmarshal([]byte(data), s)
    fmt.Println(err) // <nil>
    fmt.Println(s) // &{chancel 18 0 666}

    s.Score = 0

    var jsonData, _ = json.Marshal(s)
    fmt.Println(string(jsonData)) // {"username":"chancel","age":"18","gender":0}

}

资料引用

[[replyMessage== null?"发表评论":"发表评论 @ " + replyMessage.m_author]]

account_circle
email
web_asset
textsms

评论列表([[messageResponse.total]])

还没有可以显示的留言...
[[messageItem.m_author]] [[messageItem.m_author]]
[[messageItem.create_time]]
[[getEnviron(messageItem.m_environ)]]
[[subMessage.m_author]] [[subMessage.m_author]] @ [[subMessage.parent_message.m_author]] [[subMessage.parent_message.m_author]]
[[subMessage.create_time]]
[[getEnviron(messageItem.m_environ)]]
目录