menu E4b9a6's blog
rss_feed
E4b9a6's blog
有善始者实繁,能克终者盖寡。

Golang

作者:E4b9a6, 创建:2023-07-18, 字数:10482, 已阅:1314, 最后更新:2024-06-14

这篇文章更新于 160 天前,文中部分信息可能失效,请自行甄别无效内容。

Go(通常称为 Golang)是一种开源编程语言,由 Google 开发,设计目标是简单、高效和可并发,由 Robert Griesemer、Rob Pike 和 Ken Thompson 于 2007 年开始开发,并于 2009 年正式发布

Go常用于网络、系统、并发、分布式编程,采用Go的大型项目包括了 Docker、Kubernetes、Prometheus、Terraform 等,Go语言的主要特性是简洁、并发支持、静态类型、垃圾回收、工具链

1. 开发环境

以下基于Debian12 + VSCode

1.1. Go环境

创建golang的安装文件夹

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

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

Bash
echo 'export PATH=$PATH:$HOME/apps/golang/go/bin' >> ~/.zshrc
echo 'export GOPATH=$HOME/apps/golang' >> ~/.zshrc
echo 'export PATH=$PATH:$GOPATH/bin' >> ~/.zshrc

确认安装成功

Bash
go version

# 输出 go version go1.18.1 linux/amd64

1.2. Hello World

创建第一个项目 hello-world

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

在 hello-world 项目中创建一个 main.go 的文件,内容如下:

Go
package main

import "fmt"

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

运行 main.go

Bash
go run main.go

输出: hello world

1.3. 第三方包

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

Go
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是默认开启的,如下:

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

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

go mod 常用语法如下

TEXT
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框架

Bash
go mod init hello-world
go mod tidy

此时项目结构如下

Bash
.
├── 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 项目为例,在终端用 vscode 打开该项目

Bash
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 ,内容如下

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类型时应注意

变量声明代码如下:

Go
// 单变量声明
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,如下

Go
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语言有匿名变量,通常采用关键字 _ ,该变量不占用内存空间不分配内存

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

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

Go
*int

type Ptr *int

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

Go
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语言也对指针做了很多限制,举例说明

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

Go
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(切片) 来实现可变数组

切片的常见使用如下

Go
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会采用以下公式来进行扩容

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类型的效果

常见使用如下

Go
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,分别计算圆与长方形的体积代码如下

Go
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. 接口嵌套

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

Go
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{},使用空接口类型作为形参可以实现任意传参

TEXT
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)来判断,如下

Go
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语法较为相似

代码例子如下

Go
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值

代码如下

Go
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]])

还没有可以显示的留言...
gravatar
[[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)]]