1、 初识Golang

2020年10月18日 阅读数:9
这篇文章主要向大家介绍1、 初识Golang,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

介绍

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

咱们为何要学习Go?其实我以为是由于公司发展愈来愈快的一个必然趋势,随着发展,不少东西是nodejs不必定能很好的支持。咱们须要后端多样化,以在将来某个时段咱们能有更大的能力去面对未知的事务。mysql

Golang 的Hello World

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

package main

import "fmt"

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

和C语言相似,咱们的程序都是从 main 函数开始,使用以下语句进行编译:git

$ go build helloworld.go

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

$ ./helloworld

Hello World

下载

官网下载地址(点我)golang

下载对应系统的安装包redis

解压缩:算法

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

添加PATH环境变量:sql

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

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

$ 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语言中,指针属于比较难的一块,什么是指针? 一个指针变量,指向了一个值的地址:

image.png

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

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

s := &map[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。