前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go语言基础速刷手册

Go语言基础速刷手册

作者头像
才浅Coding攻略
发布2022-12-12 17:08:39
8680
发布2022-12-12 17:08:39
举报
文章被收录于专栏:才浅coding攻略

阿巩

跟着官方文档刷Go基础,再打怪升级!

这个“不务正业”的阿巩,今天冒着现学现卖的风险来和大家分享Go了,作为既具备C的理念又有Python 姿态的语言,怎么能不来试上一试呢!

对了,再和大伙分享一个方便查看github代码的小tip:将原网址github.com改为github1s.com可用在线编译器打开,功能强大且支持调试和运行。日拱一卒,让我们开始吧!

我们就不从安装和hello world开始了,首先来看下Go的变量和内置数据类型都有哪些。

变量声明

Go 语言与其他语言显著不同的一个地方在于,Go 语言的类型在变量后面。

方法一:指定变量类型,如果没有初始化,则变量默认为零值。

代码语言:javascript
复制
var a = "Agong"
var b int
var c bool
var d string

这里的零值指的是:数值类型为0、布尔类型为false、空字符串等。

方法二:根据值自行判定变量类型。

代码语言:javascript
复制
var d = true

方法三:在函数体内需要初始化声明时使用:=(不可以用于全局变量的声明与赋值)

代码语言:javascript
复制
msg := "Hello World!"

数据类型

空值:nil 以下几种类型为nil

代码语言:javascript
复制
var a *int
var a []int
var a map[string] int
var a chan int
var a func(string) int
var a error // error 是接口

整型类型:int

浮点数类型:float32、float64

字节类型:byte

字符串类型:string

布尔值类型:bool(true或false)

在Go语言中,字符串使用UTF8编码,如果是英文每个字符占1byte;如果是中文,一般占3字节。string是一个不可变的byte切片。

代码语言:javascript
复制
package string_test

import (
  "reflect"
  "testing"
)

func TestString(t *testing.T) {
  var s string  // 空字符串
  t.Log(s)
  s = "hello你好"
  t.Log(s, len(s)) // hello你好 11
  s = "中"
  t.Log(len(s))  // 3 是byte数
  //s[1] = '3'// string 是一个不可变的byte切片

  c := []rune(s)  // 将string字符串转为rune数组
  t.Log(len(c))  // 1

  t.Logf("中 unicode %x", c[0]) // 中 unicode 4e2d
  t.Logf("中 UTF8 %x", s)  // 中 UTF8 e4b8ad

  t.Log(reflect.TypeOf(s).Kind())  //string
}

reflect.TypeOf().Kind()可以知道某个变量的类型;

[]rune表示将string转为rune数组。

数组(array)和切片(slice)

声明数组

代码语言:javascript
复制
var arr [10] int  // 一维
var arr1 [5][5] int  // 二维

初始化声明

代码语言:javascript
复制
var arr = [5]int{1, 2, 3, 4, 5}
// 或 arr := [5]int{1, 2, 3, 4, 5}

使用索引修改数组

代码语言:javascript
复制
func main()  {
  arr := [5] int{1, 2, 3, 4, 5}
  for i := 0; i < len(arr); i++{
    arr[i] += 100
  }
  fmt.Println(arr)  // [101 102 103 104 105]
}

由于数组长度不能改变如果想拼接2个数组,或是获取子数组,需要使用切片。

切片使用数组作为底层结构,包含三个组件:容量,长度和指向底层数组的指针,切片可以随时进行扩展。

声明切片

代码语言:javascript
复制
func TestSlice(t *testing.T)  {
  slice1 := make([]float32, 0) // 长度为0的切片
  slice2 := make([]float32, 3, 5)  // [0 0 0] 长度为3容量为5的切片
  fmt.Println(len(slice1))  // 0
  fmt.Println(len(slice2), cap(slice2))  // 3 5
}

使用切片

代码语言:javascript
复制
func TestSlice(t *testing.T)  {
  slice2 := make([]float32, 3, 5)  // [0 0 0] 长度为3容量为5的切片
  slice2 = append(slice2, 1, 2, 3, 4)
  t.Log(len(slice2), cap(slice2), slice2)  // 7 12 [0 0 0 1 2 3 4]
  // 子切片
  sub1 := slice2[3:]  //[1 2 3 4]
  sub2 := slice2[:3]  //[0 0 0]
  // 合并切片
  combined := append(sub1, sub2...) 
  t.Log(combined)  // [1 2 3 4 0 0 0]
}

sub2... 表示将切片解构为 N 个独立的元素。

字典(键值对,map)

map类似于Python的dict,是一种存储键值对(Key-Value)的数据结构。

代码语言:javascript
复制
func Test(t *testing.T)  {
  // 仅声明
  m1 := make(map[string]int)
  // 声明时初始化
  m2 := map[string]string{
    "Jack": "Male",
    "Rose": "Female",
  }
  // 赋值/修改
  m1["Tom"] = 20
  t.Log(m1, m2)  // map[Tom:20] map[Jack:Male Rose:Female]
}

遍历map

代码语言:javascript
复制
func TestTravelMap(t *testing.T)  {
  m1 := map[int]int{1: 2, 2: 4, 3: 9}
  for k, v := range m1{
    t.Log(k, v)
  }
}
//3 9
//1 2
//2 4

指针(pointer)

一个指针变量指向了一个值的内存地址,声明时使用符号*指明该变量为指针;对于已存在的变量,使用符号&获取该变量地址。

代码语言:javascript
复制
package main

import "fmt"

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 变量的地址是:c00000a098
//ip变量存储的指针地址:c00000a098
//*ip变量的值:20

一般来说,指针通常在函数传递参数,或者给某个类型定义新的方法时使用。Go语言中参数是按值传递的,如果不使用指针,函数内部会拷贝一份参数的副本,对参数的修改并不会影响到外部变量的值。如果使用指针,则会影响外部变量的值。

代码语言:javascript
复制
package main

import "fmt"

func add(num int)  {
  num += 1
}
func realAdd(num *int)  {
  *num += 1
}
func main()  {
  num := 100
  add(num)
  fmt.Println(num)  // 100 num没有变化

  realAdd(&num)
  fmt.Println(num)  // 101 指针传递 num被修改
}

在某种情况下,我们需要保存数组,我们就需要用到指针。

代码语言:javascript
复制
const MAX = 3  // MAX为常量
func main()  {
  a := []int{10, 100, 200}
  var i int
  var ptr [MAX] *int

  for i = 0; i < MAX; i++{
    ptr[i] = &a[i]  // 整数地址赋值给指针数组
  }
  for i = 0; i < MAX; i++{
    fmt.Printf("a[%d] = %d\n", i, *ptr[i])
  }
}
//a[0] = 10
//a[1] = 100
//a[2] = 200

流程控制(if, for, switch, select)

条件语句if else

代码语言:javascript
复制
func TestIfElse(t *testing.T)  {
  age := 18
  if age < 18 {  //可以简写为 if age := 18; age < 18{}
    t.Log("Kid")
  }else {  // 注意else需要和if的}在同一行
    t.Log("Adult")
  }
}

switch

代码语言:javascript
复制
func TestSwitch(t *testing.T)  {
  type Gender int8
  const (
    MALE Gender = 1
    FEMALE Gender = 2
  )
  gender := MALE
  switch gender {
  case MALE:
    t.Log("Male")
    //fallthrough
  case FEMALE:
    t.Log("Female")
    //fallthrough
  default:
    t.Log("Unknown")
  }
}
// Male

这里使用了type关键字定义了一个新的类型Gender,使用const定义了MALE 和 FEMALE 2 个常量。Go 语言的 switch 不需要 break,匹配到某个 case,执行完该 case 定义的行为后,默认不会继续往下执行。如果需要继续往下执行,需要使用 fallthrough。

case后还支持多种条件:

代码语言:javascript
复制
func TestSwitchMultiCase(t *testing.T)  {
  for i:=0; i<5; i++{
    switch i {
    case 0, 2:  //case后支持多项判断
      t.Log(i, "It is Even")
    case 1, 3:
      t.Log(i, "It is Odd")
    default:
      t.Log(i, "It is not in 0-3")
    }
  }
}
//0 It is Even
//1 It is Odd
//2 It is Even
//3 It is Odd
//4 It is not in 0-3

for

对数组(arr)、切片(slice)、字典(map) 使用 for range 遍历:

代码语言:javascript
复制
func TestForRange(t *testing.T)  {
  // 遍历数组
  nums := []int{10, 20, 30, 40}
  for _, num := range nums{
    t.Log(num)
  }
  //10
  //20
  //30
  //40
  
  // 遍历字典
  m2 := map[string]string{
    "Jack": "Male",
    "Rose": "Female",
  }
  for k, v := range m2{
    t.Log(k, v)
  }
  //Jack Male
  //Rose Female
}

由于在Go语言中只有用for关键字来控制循环,那如何实现while循环的效果呢

代码语言:javascript
复制
func TestWhileLoop(t *testing.T) {
  n := 0
  for n < 5 {
    t.Log(n)
    n++
  }
}
//0
//1
//2
//3
//4

select

select是Go中的一个控制语句,每个case必须是一个通信操作,要么是发送要么接收。select 会循环检测条件,如果有满足则执行并退出,否则一直循环检测。(以下例子中的channel之后会说到)

代码语言:javascript
复制
func TestSelect(t *testing.T) {
  var c1, c2, c3 chan int
  var i1, i2 int
  select {
    case i1 = <-c1:
      fmt.Print("received", i1, "from c1\n")
    case c2 <- i2:
      fmt.Print("sent", i2, "to c2\n")
    case i3, ok := <-c3:
      if ok{
        fmt.Print("receive", i3, "from c3\n")
      } else {
        fmt.Print("c3 is closed\n")
      }
    default:
      fmt.Print("no communication\n")
  }
}
//no communication

函数(func)

一个函数使用关键字func定义,参数可以有多个,返回值也可以有多个。package main中的func main()约定为可执行程序的入口。

代码语言:javascript
复制
func funcName(param1 Type1, param2 Type2, ...) (return1 Type3, ...) {
    // body
}
代码语言:javascript
复制
package main

import "fmt"

func max(num1, num2 int) int  {
  /*声明局部变量*/
  var result int
  if num1 > num2{
    result = num1
  }
  if num1 < num2{
    result = num2
  }
  return result
}

func main()  {
  /*定义局部变量*/
  var a int = 100
  var b int = 200
  var res int

  /*调用函数并返回最大值*/
  res = max(a, b)
  fmt.Printf("最大值是:%d\n", res)
}
//最大值是:200

错误处理

函数在实现过程中,如果出现不能处理的错误,可以返回给调用者处理,比如调用标准库的os.open读取文件,os.open有2个返回值,第一个是*file,第二个是error,如果调用成功error的值是nil;如果失败,例如文件不存在,可以通过error知道具体的错误信息。

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

代码语言:javascript
复制
type error interface {
    Error() string
}
代码语言:javascript
复制
package main

import (
  "fmt"
  "os"
)

func main()  {
  _, err := os.Open("filename.txt")
  if err != nil{
    fmt.Println(err)
  }
}
//open filename.txt: The system cannot find the file specified.

使用error.New返回自定义的错误

代码语言:javascript
复制
func hello(name string) error {
  if len(name) == 0{
    return errors.New("error: name is null")
  }
  fmt.Println("Hello", name)
  return nil
}

func main()  {
  if err := hello(""); err != nil{
    fmt.Println(err)
  }
}
//error: name is null

error往往是能预知的错误但是也可能出现一些不可预知的错误,例如数组越界,这种错误可能会导致程序非正常退出,在 Go 语言中称之为 panic。

代码语言:javascript
复制
package main

import "fmt"

func get(index int) int {
  arr := [3]int{2, 3, 4}
  return arr[index]
}

func main()  {
  fmt.Println(get(5))
  fmt.Println("finished")
}
//panic: runtime error: index out of range [5] with length 3
//goroutine 1 [running]:
//Process finished with the exit code 2

panic用于主动抛出错误,recover用来捕获panic抛出的错误。发生panic后,程序会从调用panic的函数位置或发生panic的地方立即返回,逐层向上执行函数的defer语句,然后逐层打印函数调用堆栈,直到被recover捕获或运行到最外层函数。defer 和 recover类似于Python中的try...catch。

recover用来捕获panic,阻止panic继续向上传递。recover()和defer一起使用,但是defer只有在后面的函数体内直接被掉用才能捕获panic来终止异常,否则返回nil,异常继续向外传递。

代码语言:javascript
复制
func get(index int) (ret int) {
  defer func() {
    if r := recover(); r != nil {
      fmt.Println("Some error happened!", r)
      ret = -1
    }
  }()
  arr := [3] int{2, 3, 4}
  return arr[index]
}
func main()  {
  fmt.Println(get(5))
  fmt.Println("finished")
}
//Some error happened! runtime error: index out of range [5] with length 3
//-1
//finished
  • 在 get 函数中,使用 defer 定义了异常处理的函数,在协程退出前,会执行完 defer 挂载的任务。因此如果触发了 panic,控制权就交给了 defer。
  • 在 defer 的处理逻辑中,使用 recover,使程序恢复正常,并且将返回值设置为 -1,在这里也可以不处理返回值,如果不处理返回值,返回值将被置为默认值 0。

结构体,方法和接口

结构体(struct)

结构体类似于其他语言的class,是一个由相同类型或不同类型的数据构成的数据集合。

代码语言:javascript
复制
package main

import "fmt"

type Student struct {
  name string
  age int
}

func (stu *Student) hello(person string) string {
  return fmt.Sprintf("Hello %s, I am %s", person, stu.name)
}
func main()  {
  stu := &Student{
    name: "Jack",
  } // 实例化
  msg := stu.hello("Rose")  // 调用方法:实例名.方法名(参数)
  fmt.Println(msg)
}
// Hello Rose, I am Jack

实现方法与实现函数的区别在于,func 和函数名hello 之间,加上该方法对应的实例名 stu 及其类型 *Student,可以通过实例名访问该实例的字段name和其他方法了。

此外,还可以通过new实例化

代码语言:javascript
复制
func main() {
  stu2 := new(Student)
  fmt.Println(stu2.hello("Alice")) // hello Alice, I am  , name 被赋予默认值""
}

接口(interfaces)

一般而言,接口定义了一组方法的集合,接口不能被实例化,一个类型可以实现多个接口。Go 语言中,并不需要显式地声明实现了哪一个接口,只需要直接实现该接口对应的方法即可。

代码语言:javascript
复制
package main

import "fmt"

type Person interface {
  getName() string
}
type Student struct {
  name string
  age int
}

func (stu *Student) getName() string {
  return stu.name
}

func (stu *Student) getAge() int {
  return stu.age
}

type Worker struct {
  name string
  gender string
}

func (w *Worker) getName() string {
  return w.name
}

func main()  {
  var p Person = &Student{
    name: "Jack",
    age: 24,
  }
  fmt.Println(p.getName())  //Jack

  // 实例可以强制类型转换为接口,接口也可以强制类型转换为实例。
  stu := p.(*Student)
  fmt.Println(stu.getAge())  //24
}

为确保某个类型实现了某个接口的所有方法,可以使用下面的方法进行检测,如果实现不完整,编译期将会报错。

代码语言:javascript
复制
var _ Person = (*Student)(nil)
var _ Person = (*Worker)(nil)
  • 将空值 nil 转换为 *Student 类型,再转换为 Person 接口,如果转换失败,说明 Student 并没有实现 Person 接口的所有方法。

空接口

如果定义了一个没有任何方法的空接口,那么这个接口可以表示任意类型。

代码语言:javascript
复制
func main()  {
  m := make(map[string]interface{})
  m["name"] = "Jack"
  m["age"] = 24
  m["scores"] = [3] int{90, 97, 92}
  fmt.Println(m)  // map[age:24 name:Jack scores:[90 97 92]]
}

并发编程(goroutine)

goroutine 是轻量级线程,通过 go 关键字即可开启一个新的运行期线程 goroutine 。同一个程序中的所有 goroutine 共享同一个地址空间。

代码语言:javascript
复制
package main

import (
  "fmt"
  "time"
)

func say(s string)  {
  for i := 0; i < 5; i++{
    time.Sleep(100 * time.Millisecond)
    fmt.Println(s)
  }
}

func main()  {
  go say("world")
  say("hello")
}
//hello
//world
//world
//hello
//hello
//world
//hello
//world
//hello

执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行。

Go 语言提供了 sync 和 channel 两种方式支持协程(goroutine)的并发。

例如我们希望并发下载 N 个资源,多个并发协程之间不需要通信,那么就可以使用 sync.WaitGroup,等待所有并发协程执行结束。

代码语言:javascript
复制
package main

import (
  "fmt"
  "sync"
  "time"
)

var wg sync.WaitGroup

func download(url string)  {
  fmt.Println("start to download", url)
  time.Sleep(time.Second)  //模拟耗时操作
  wg.Done()  //减去一个计数
}

func main()  {
  for i := 0; i < 3; i++  {
    wg.Add(1)  //为wg添加一个计数
    go download("a.com/" + string(i+'0'))  //启动新的协程并发执行 download 函数
  }
  wg.Wait()  //等待所有的协程执行结束
  fmt.Println("Done!")
}
//start to download a.com/2
//start to download a.com/1
//start to download a.com/0
//Done!

通道(channel)

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

代码语言:javascript
复制
ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
           // 并把值赋给 v

声明通道

代码语言:javascript
复制
ch := make(chan int)

默认情况下,通道是不带缓冲区的。

代码语言:javascript
复制
package main

import "fmt"

func sum(s [] int, c chan int)  {
  sum := 0
  for _, v := range s{
    sum += v
  }
  c <- sum  //把sum发送到通道c
}

func main()  {
  s := []int{7, 2, 8, -9, 4, 0}

  c := make(chan int)
  go sum(s[:len(s)/2], c)
  go sum(s[len(s)/2:], c)
  x, y := <-c, <-c  // 从通道c中接收

  fmt.Println(x, y, x+y)  //-5 17 12
}

加入通道缓冲区

代码语言:javascript
复制
ch := make(chan int, 100)

通道遵循先进先出原则。

不带缓冲区的通道是同步的,即在向通道发送值时,必须及时接收,且必须一次接收完成。

而带缓冲区的通道是异步的,它仅会以缓冲区满而阻塞,直到先塞发送到通道的值被从通道中接收才可以继续往通道传值。

代码语言:javascript
复制
func main() {
  ch := make(chan int, 2)

  ch <- 1
  a := <-ch
  ch <- 2
  ch <- 3

  fmt.Println(<-ch)
  fmt.Println(<-ch)
  fmt.Println(a)
}
//2
//3
//1

如上面的例子,最多只能让同时在通道中停放2个值,想多传值,就需要把前面的值提前从通道中接收出去。

我们再将下载资源的例子实现协程之间阻塞等待并发协程返回消息:

代码语言:javascript
复制
var ch = make(chan string, 10) // 创建大小为 10 的缓冲通道

func download(url string) {
  fmt.Println("start to download", url)
  time.Sleep(time.Second)
  ch <- url // 将 url 发送给通道ch
}

func main() {
  for i := 0; i < 3; i++ {
    go download("a.com/" + string(i+'0'))
  }
  for i := 0; i < 3; i++ {
    msg := <-ch // 等待信道返回消息。
    fmt.Println("finish", msg)
  }
  fmt.Println("Done!")
}
//start to download a.com/2
//start to download a.com/1
//start to download a.com/0
//finish a.com/2
//finish a.com/0
//finish a.com/1
//Done!

包(Package)和模块(Modules)

Go语言没有像其它语言一样有public、protected、private等访问控制修饰符,它是通过字母大小写来控制可见性的。区分粒度是包(package)。如果定义的常量、变量、类型、接口、结构、函数等的名称是大写字母开头表示能被其它包访问或调用(相当于public),非大写开头就只能在包内使用(相当于private,变量或常量也可以下划线开头)。

参考:

https://golang.org/

https://geektutu.com/post/quick-golang.html

https://www.runoob.com/go/go-tutorial.html

END

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 才浅coding攻略 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 包(Package)和模块(Modules)
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档