10

一、 初识Golang

 3 years ago
source link: https://studygolang.com/articles/31183
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

介绍

Go(Golang)是谷歌开发的一种 静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言。

我们为什么要学习Go?其实我觉得是因为公司发展越来越快的一个必然趋势,随着发展,很多东西是nodejs不一定能很好的支持。我们需要后端多样化,以在未来某个时段我们能有更大的能力去面对未知的事务。

Golang 的Hello World

还记得以前学习C语言的时候,老师都是从Hello World开始讲起,今天我们也是从这里开始我们的Golang之旅。

package main

import "fmt"

func main() {
  fmt.Println("Hello World")
}

和C语言类似,我们的程序都是从 main 函数开始,使用如下语句进行编译:

$ go build helloworld.go

编译完了之后,会在当前文件生成一个 helloworld 可执行文件,执行改文件即可打印:

$ ./helloworld

Hello World

下载

官网下载地址( 点我 )

下载对应系统的安装包

解压缩:

tar -C /usr/local -xzf go1.15.2.linux-amd64.tar.gz

添加PATH环境变量:

export PATH=$PATH:/usr/local/go/bin

安装好了使用下面命令测试:

$ go version

go version go1.15.2 linux/amd64

我们可以使用 go env 查看go的环境变量,我们先重点关心这个环境变量:

GOPROXY

Go 除了自身带了很多包,社区也有很多开源项目的包,大部门都是在github、google、等域名上面,如果你的terminal没有翻墙,拉取会很慢,这个时候,我们可以设置这个环境变量(非系统环境变量):

$ go env -w GO111MODULE=on
$ go env -w GOPROXY=https://goproxy.cn,direct

这样,拉取所有的包都走的是七牛云。

Golang 的语法

Golang 是一门上手非常快、并发性能非常高的语言,我们先来介绍Go的基本语法。

package

每份 .go 代码,都需要在文件的第一行指定包名,一般来说,都是当前所在文件夹的名字,在同一个文件夹下的Go文件,包名是一致的。包名不能使用特殊字符,比如 - 等,一般都是小写并且简短

其中有个例外,就是 package main ,main 包在一个文件夹下只能有一个。

包的导入有两种方式:

import "github.com/go-redis/redis"
import "context"

// OR

import (
    "github.com/go-redis/redis"
    "context"
)

我们推荐使用第二种写法,有时候,我们可能只想引用某个包,让其执行 init 函数,而不会用到包里面的函数,可以这样做:

import (
    _ "github.com/go-sql-driver/mysql" // 只会执行 mysql 包下的init函数
)

main 函数

main 函数是我们项目的入口程序,如果一个go代码被编译,它没有main函数,是无法编译出可执行文件的。

main 函数的写法是:

func main() {
    // do something
}

语句

一个 statement 就是一行,不需要结尾的分号

比如:

func main() {
    fmt.Println("Hello Go!") // 这就是一条语句
}

注释

和大部分语言的注释一样,Go使用 // 代表单行注释,使用 /** **/ 代表多行注释

比如:

func main() {
    // 我是单行的注释
    /**
    我是
    多行
    的
    注释
    **/
}

基本类型

Go 是强类型的语言,不会像我们目前使用的nodejs一样,一个变量可以随意赋任意类型的值。在Go中,一旦定义了某个变量的类型,则这个变量只能赋予该类型的值。

  • 布尔类型 bool 布尔的值只能是 true 或者 false ,和nodejs不同,0 不代表false。
  • 字符串类型 string 字符串必须是由双引号包含起来的字符集合。
  • 数字类型 见下

数字类型 可以分为 整数类型、浮点数类型和一些其它特殊意义的类型

整数类型

序号 类型 描述 1 uint8 无符号8位整数(0-255) 2 uint16 无符号16位整数(0-65535) 3 uint32 无符号32位整数(0-4294967295) 4 uint64 无符号64位整数(0-18446744073709551615) 5 int8 有符号8位整数(-128到127) 6 int16 有符号16位整数(-32768 到 32767) 7 int32 有符号 32 位整型 (-2147483648 到 2147483647) 8 int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点数类型

序号 类型 描述 1 float32 IEEE-754 32位浮点型数 2 float64 IEEE-754 64位浮点型数 3 complex64 32位实数和32位虚数 4 complex128 64位实数和64位虚数

其它数字类型

序号 类型 描述 1 byte 类似 uint8 2 rune 类似int32 3 uint 无符号整数,取决于系统,系统是32位则是32位,系统是64位则是64位整数 4 int 有符号整数,和uint一样 5 uintptr 无符号整型,用于存放一个指针

变量

变量名的规则和其它语言都类似, 只能由字母、数字和下划线取名,且不能以数字开头 :

var variable data_types = value

其实声明变量的方式由很多种

// 第一种 这种方式必须要指定类型
// 如果a没有被赋值,则a的值默认是该类型的零值,int->0,string->"",bool->false
var a int
a = 1

// 第二种 声明的时候直接赋值,如果没有给定类型,会自动推导出来
var a int = 1
var a = 1 // fmt.Printf("%T", a) => int

// 第三种 声明常量,常量声明的时候必须要赋值,类型可以写可以不写,会自动推导,常量不可被修改
const a = 1
const (
    a = 100
    b
) // a、b都是int 值都是100,只有const可以这样用
const (
    a = iota  // 0
    b         // 1
    c         // 2
    d = "pdf" // iota+1
    e = 100   // iota+1
    f = iota  // 5
    g         // 6
) // iota 是一个特殊常量,z在下一行的时候会自动递增,只在一个const()作用域有效,用作枚举很方便

// 第四种 自动推导
a := 1
a := ""

变量的声明是可以多个变量可以一起声明的:

var (
    a,
    b,
    c int
) // a、b、c都被声明为int 类型

var (
    a,
    b,
    c int
    d,
    e,
    f string
) // a、b、c都被声明为int 类型,d、e、f都被声明为字符串类型

a, b := 1, "" // a 是int,b是string,这种方式最常用,因为函数的返回值可以是多个

运算符

运算符用于程序在执行算术运算或者逻辑运算时使用。

Go内置的运算符有:

  • 算术运算符
  • 关系运算符
  • 逻辑运算符
  • 位运算符
  • 赋值运算符
  • 其它运算符

算术运算符

假设A=10 B=20

运算符 描述 实例 + 相加 A+B = 30 - 相减 B - A=10 * 相乘 A * B = 200 \ 相除 B / A = 2 % 求余 B % A = 0 ++ 自增 A++ // A=> 11 -- 自减 B-- // B=> 19

关系运算符

假设A=10 B=20,关系运算符的结果时bool类型的值

// ==
fmt.Println(A == B) // false

// !=
fmt.Println(A != B) // true

// > < >= <=
fmt.Println(A > B) // false
fmt.Println(A < B) // true
fmt.Println(A >= B) // false
fmt.Println(A <= B) // true

逻辑运算符

假设 A = true B = false

// && 逻辑AND
fmt.Println(A && B) // => false

// || 逻辑或
fmt.Println(A || B) // => ture

// ! 逻辑非
fmt.Println(!A, !B) // => false true

位运算符

  • 按位与 &
  • 按位或 |
  • 按位异或 ^: 1^1=0;1^0=1;0^0=0;
  • 左移 <<
  • 右移 >>

赋值运算符

=、+=、-=、*=、=、%=、<<=、>>=、&=、^=、|=

其它运算符

指针取地址: &, 取值 *

条件语句

Go语言提供下面几种条件语句:

  • if 语句
  • switch 语句
  • select 语句

if 语句

Go 语言中,if 语句的语法:

if 布尔条件表达式 {
    statement
} else if 布尔条件表达式 {
    statement
} else {
    statement
}

注意,这里的条件表达式不需要括号,举个实例:

package main
func main() {
    a := 1
    b := 2
    if a < b { // if 语句有这样的语法糖 if a:= func(); a < 10 {}
        fmt.Println("a < b")
    } else if a == b {
        fmt.Println("a == b")
    } else {
        fmt.Println("a >  b")
    }
}

switch 语句

Go 语言的switch语句很强大,和nodejs 的不太一样,Go语言中的Switch语法:

switch expression {
    case condition:
        statement
    default:
        statement
}

首先,expression 可以是任何类型的值,如果没有写,则默认是 true, condition 必须要和expression的类型一致,否则报错,并且condition可以为多个,以逗号隔开,default 子句表示当case都没有满足的条件的时候执行,同时最大的一点不同之处,没有break语句,Go程序的 switch 每一个 case 执行完了就退出 switch 而不会继续向下执行,如果需要向下执行,则需要在case{}中最后加上一句: fallthrough

比如:

switch "2" {
    case "1", "2", "3":
        fmt.Println("1 2 3")
        fallthrough
    case "4":
        fmt.Println("4")
    default:
        fmt.Println("default")
} // => 1 2 3
  // => 4

select 语句

select 语句一般用于超时控制,是Go的一个控制结构,每个case都是一个channel操作,select 将随机选取一个可以运行的case执行,如果没有可以运行的case,则select语句会阻塞,直到有可以执行的case。默认子句总是可以执行的。

语法如下:

select {
    case <- ch: // 后面会讲,这个是 channel 这一块的知识
        statement
    default:
        statement
}

不过一般我们都不会用default,就像上面讲的,我们一般是用来做超时控制的,举个例子:

package main

import (
    "fmt"
    "time"
)

func sleep(n int, res chan<- string) {
    time.Sleep(time.Duration(n) * time.Second)
    res <- "获取到了结果" // 管道里面设置数据
}

func main() {
    timeout := time.After(time.Duration(3) * time.Second)
    res := make(chan string)
    go sleep(2, res)
    select {
    case <- timeout:
        fmt.Println("超时了")
    case val := <- res: // 接收管道的数据
        fmt.Println(val)
    }
}

这里,我们请求一个函数,同时设置一个3s的超时,如果函数3s内没有返回数据,则select 会执行超时这个case,否则就执行了取结果的数据。当然,这个例子是没有代表性的,因为实际工程上要比这个复杂一点,因为要涉及到资源的释放之类的,后面会讲到。

循环语句

Go的循环语句和nodejs也不太一样,我们先看语法:

// 第一种
for init; condition; post { // 注意,这里可以省略任意一项,如果只有condition可以不要分号,
    statement
}
// 我们经常会使用死循环+select来做一下事情
for {
    select {
        case xxx
    }
}

// 第二种 
for k,v := range mapValue { 
    
}
// 由于Go定义了变量,就必须要使用,所以不需要的变量可以用 _ 代替,比如
for _, v := range mapValue {
    
}

函数

Go 中函数的声明从 func 关键字开始,语法如下:

func funcName(v datatypes) datatypes {
    
}

func 关键字后面接的是函数名字,函数名字一般是用驼峰命名,如果是小驼峰,则这个函数只能在当前文件被调用,如果是大驼峰,则可以被其它包引入调用。

括号里面的是形参,形式为 变量名 变量类型 。Go的返回值也必须要定义类型,定义类返回值类型,则必须要有返回值。

举个例子:

func f1(a int, b string) string {} // 传了一个int, 一个string 返回值是string

func f2(a, b int, c, d string) {} // 如果两个参数相邻并且类型一致,则只需要在最后一个变量写类型

func f3() (string, string) {} // Go支持返回多个参数,这里返回了两个string类型的值

func f4() (a , b int) {} // 这里表示已经定义好了返回值的变量,只要在代码里面对a,b赋值就可以,如果没有赋值,则返回对应类型的零值

!!!非常重要!!!

函数传参一般有两种:

  • 值传递是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数
  • 引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。

而Golang使用的是值传递

https://goinbigdata.com/golan...

每个go文件都可以有一个 init 函数,在被导入包的时候就会执行,并且只会执行一次,被多次导入也只会执行一次。

func init() {
    
}

数组

数组是一种具有唯一的相同的数据类型、固定长度的数据项序列,这个类型可以是整型、字符串或者任意自定义的结构体。

定义数组必须要指定长度,语法如下:

var variables [size] variable_type

以下是例子:

var balance [10] float32 // 长度为10的数组,初始值是[0,0,0,0,0,0,0,0,0,0]

var balance = [3]int {1, 2, 3} // 初始化,知道长度

var balance = [...]int {1, 2, 3, 4, 5} // 初始化,如果不知道长度,可以自动推断

指针

和 nodejs 不同的是,Go 是有指针的,和C一样,都是使用 & 进行取地址,使用 * 定义指针变量

举个例子:

var a int = 3
var p *int = &a // p := &a

在C语言中,指针属于比较难的一块,什么是指针? 一个指针变量,指向了一个值的地址:

比如一个变量 C 保存了字符 'K',地址在 0x11A,而一个指向c的指针变量p,它的值就是C的地址11A。

通过一个实例来看怎么使用指针:

s := ↦[string]string{"a": "a", "b": "b"}

v, ok := (*s)["a"]

结构体

结构体是Go中非常重要的一个模块,后面的开发一定会用的到,不管是创建Service还是创建Orm的model。

数组是只能存储一个类型固定长度的值,结构体呢?可以存储多个不同类型的值,简单来说,结构体就是一系列不同或者相同类型的集合。

结构体语法定义如下:

type struct_name struct {
    field1 data_types
    field2 data_types
    ...
}

举个例子:

type User struct {
    Name string
    Age uint8
    ID int
    Event [10]string
}

这样我们就定义了一个结构体,声明使用如下:

// 不是很推荐的写法
user := User{"pdf", 8, 123, [10]string{"a", "b"}} // 按定义顺序依次填入

// 推荐写法
user := User{
    Name: "pdf"
    Age: 8,
} // 可以忽略掉不需要的字段,这样没有赋值的就是对应的零值

声明了之后,使用方式如下:

user.Name = "ghj"
fmt.Println(user.Age)

这里有一个语法糖,关于指针的,就是说,对于结构体的指针,我们可以忽略 *

u := &user
fmt.Println(u.Name, (*u).Name)

切片(slice)

数组长度是固定的,对于长度不知道或者不固定的数据,我们就很难使用数组去实现这种业务。

所以就有了切片(动态数组),切片长度是不固定的,切片的定义和数组的定义很类似,不过不需要长度

arr := [10]int{}
slice := []int{}

fmt.Printf("arr: %T, slice: %T", arr, slice) // arr: [10]int, slice: []int

// 还可以用make创建切片
slice := make([]type, len) // 指定初始长度
// 还可以指定容量
slice := make([]type, len, cap)

切片初始化和赋值:

a := []int{1, 2} // 声明的时候直接初始化
a = append(a, 1) // 追加

arr := [10]int{}
slice := arr[start:end] // 从数组里面切,从start算,到end-1

现在,我们说了数组和切片,可以开始说一下长度和容量了。

Go提供了 len(v Type)cap(v Type) 查看数组和切片的长度以及容量

数组的长度和容量都是固定的!

切片的长度表示当前切片值的个数,切片的容量是什么呢?

我们需要从切片的本质去看:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}

这个就是切片的结构体,Data 指向数组的指针,Len表示当前切片的长度,Cap表示Data数组的大小。

用一个实际的例子来说:

b := [4]int{}
d := b[0: 3]

fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ① b=[0 0 0 0], d=[0 0 0], len(d)=3, cap(d)=4

d[0] = 1
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ② b=[1 0 0 0], d=[1 0 0], len(d)=3, cap(d)=4

d = append(d, 2)
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ③ b=[1 0 0 2], d=[1 0 0 2], len(d)=4, cap(d)=4

d[0] = 3
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ④ b=[3 0 0 2], d=[3 0 0 2], len(d)=4, cap(d)=4

d = append(d, 4)
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ⑤ b=[3 0 0 2], d=[3 0 0 2 4], len(d)=5, cap(d)=8

d[0] = 5
fmt.Printf("b=%v, d=%v, len(d)=%v, cap(d)=%v\n",b, d, len(d), cap(d))
// ⑥ b=[3 0 0 2], d=[5 0 0 2 4], len(d)=5, cap(d)=8

d是切片,这个时候Data指向的就是b这个数组,一个六个输出,依次说一下:

  1. d切了[0:3],所以长度是3,容量是b数组的长度,也就是4,当没有赋值时,int的默认零值是0,所以b=[0,0,0,0],d=[0,0,0]
  2. 由于切片的Data指向了b,所以修改是修改b数组的值,所以 b = [1, 0, 0, 0],d = [1, 0, 0]
  3. append函数用于切片末尾追加一个值,注意,这个不是直接修改d,必须要重新用d接收返回值,所以此时d的长度是4,容量也是4,由于d的Data指向b,所以b和d的值都是 [1, 0, 0, 2]
  4. 由于d的Data指向b,所以b和d的值都是[3, 0, 0, 2]
  5. 这个时候,d再次追加了一个值,这个时候,b=[3,0,0,2],和上一次打印是一致的,而d的值为[3, 0, 0, 2, 4],追加成功,并且长度确实加了1,但是容量却从4变为了8.可能看到这里大家就会疑惑了,d的Data不是指向了b吗,为什么这一次b没有被修改呢?这是因为数组是长度不变的,切片是动态数组,当切片容量不够时,就会新申请一片连续的内存作为Data的指向,并且将原来的值复制过去,这个就是切片的扩容(扩容算法不展开讲),所以一旦发生了扩容,切片的指向就会发生变化
  6. 所以这个时候,再次修改d,也不会影响b了,所以b=[3, 0, 0, 2],d=[5, 0, 0, 2, 4]

切片的零值是 nil

就像上面说的,直接切片一个数组或者别的切片得到的新切片,其实Data指向的还是原来的,但是有时候我们不要这个引用,因为我们要传参的时候想要修改这个值但是不能影响实参,所以我们就需要拷贝切片:

old := []int{1, 2, 3}
newSlice := make([]int, len(old), 2 * cap(old)) // 使用make函数,指定长度和容量,注意,长度一定小于等于容量
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[1 2 3], newSlice=[0 0 0]

copy(newSlice, old) // 使用copy函数复制,第一个参数是目标,第二个参数是被拷贝的值,newSlice的长度至少都要等于old的长度,否则会截断
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[1 2 3], newSlice=[1 2 3]

old[0] = 2
fmt.Printf("old=%v, newSlice=%v\n", old, newSlice)
// old=[2 2 3], newSlice=[1 2 3]
// old修改成功,但是newSlice不会被修改

Map 集合

Go 种的Map集合和我们使用的Nodejs种的对象类似,它就是一组无序的键值对的集合。

var m map[string]int 
m := make(map[string]int)// 键是string类型,值是int类型

// 初始化
var m = map[string]int{"pdf": 100}
m["ghj"] = 99
fmt.Println(m) // map[ghj:99 pdf:100]

Map值的获取就比较有意思了,我们前面有一直说到,一个变量定义了,它都会有自己对应的零值,所以在map获取值的时候,如果某个key是不存在的,则返回了value类型对应的零值,比如:

v := m["none"]
fmt.Println(v) // 0

这个时候是有问题的。大家思考一下

如果我本来就是有一个存在的key,并且值我就是设置为0呢?

m["hz"] = 0
v := m["hz"]
fmt.Println(v) // 0

这个时候就没有办法区分这个key的值到底是默认的零值还是我们自己设置的零值。所以我们需要用另外一种写法:

if v, ok := m["none"]; ok {
    fmt.Println(v)
}

v, ok := m["none"]
fmt.Println(v, ok) // 0 false

还记得if这个写法吗?忘了的同学可以往前面看看。这个时候,我们多接受了一个ok参数,这个参数的类型是bool,如果为true表示key存在,如果为false表示key不存在,获取的是零值。

map的key是可以用 == 或者 != 作比较的类型(map和interface{}这两种是我知道的不可比较的两种动态类型)。比如:

m := map[bool]bool{true: true}

m := map[struct{}]map[string]int

刚刚说过,key存不存在可以用第二个返回值判断,如果说后面我们需要删掉一个key,可以使用 delete 函数:

m := map[string]int{"pdf": 1}
delete(m, "pdf")
v, ok := m["pdf"]
fmt.Println(v, ok)

接口 interface

Go语言提供了另外一种数据类型 即接口,他把所有具有共性的方法定义在一起,任何其它类型只要实现了这些方法(接口定义里面的全部)就相当于实现了这个接口。

比如:

type UserInferface interface { // 定义了一个用户接口
    GetUserInfo(userId int) (UserInfoDTO, error)
    UpdateUserInfo(vo UserVo) (UserInfoDTO, error)
}

type UserService struct{} // 定义一个用于实现用户接口的结构体

func (service *UserService) GetUserInfo(userId int) (UserInfoDTO, error) {
    
}

func (service *UserService) UpdateUserInfo(vo UserVO) (UserINfoDTO, error) {
    
}

func main () {
    s := &UserService{}
    s.GetUserInfo()
    
}

这样,UserService就实现了UserInterface这个接口。

错误处理

Go 语言通过内置的错误接口提供了非常简单的错误处理机制(Go 中没有所谓try catch)。

error 类型是一个接口类型,定义如下:

type error interface {
    Error() string
}

通常我们都会使用 errors 包来返回一个错误:

func demo() error {
    return errors.New("demo 发生错误")
}

err := demo()
if err != nil {
    fmt.Println(err) // fmt.Println(err.Error())
}

知道了 error 类型是一个接口,所以我们可以自定义error类型:

func (us * User1Service)Error() string {
  return us.Name + "又错了"
}
func demo() error {
  return &User1Service{
    Name: "pdf",
  }
}

err := demo()
fmt.Println(err) // pdf又错了

Go 并发

并发这里,是初识Golang的最后一节,我们选择Go的一个很大原因就是Go的并发能力很强,并且使用非常简单!

简单到什么程度?

go someFunc()

就这么简单,只需要被调用函数前面加一个 go 关键字就可以了。

go 关键字其实是开启一个叫做 goroutine 东西,这个其实叫做协程。

main函数就是一个主协程。

举个例子:

func output(intput int)  {
  fmt.Println(intput)
}

func main() {
  for i := 0; i < 10; i++ {
    go output(i)
  }
  for i := 10; i < 20; i++ {
    go output(i)
  }
  time.Sleep(time.Duration(1) * time.Second)
}

当你运行的时候,可以看到,每一次输出的结果都是不一致的。加一个睡眠是因为,主协程运行结束,则程序就退出了,所以还没有运行完的协程就没有运行机会了。

这样来看,有没有感觉和我们的异步是一样的,哈哈。

结束语

今天讲的东西,我全部都没有深入去讲,希望这一篇文章,能让我们大家从一个noder,从此也可以叫为 gopher。

有疑问加站长微信联系

iiUfA3j.png!mobile

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK