Go 语言基础整理

251 阅读8分钟

变量

代表可变的数据类型,定义方式如下

var i int = 10
var i = 10
var (
    i int = 10
    j int = 10
)
var (
    i = 10
    j = 10
)
i := 10
  1. 变量可以赋值,直接 i = 20 修改即可
  2. 变量定义了就必须使用
  3. Go 是静态类型语言,不能在运行期间改变变量类型
i := 10
i = true

是错的

i := 10
i = 11

是对的

常量

在编译期就确定好了,不能修改,常量只允许布尔型、字符串和数字类型

const name = "Mike"
const one = 1
const bf = true

指针

变量的内存地址,通过 & 获取指针,直接输出获取地址,加 * 获取指针指向的变量值

package main

import "fmt"

func main() {
   i := 10
   pi := &i
   fmt.Println(*pi)
   fmt.Println(pi)
}

引用数据类型

数组

数组是聚合类型,他们的值由内存中的一组变量构成,数组的元素具有相同的类型 静态数据结构,长度固定 内置函数 len 用于返回数组中元素的个数

package main

import "fmt"

func main() {
   var a [3]int
   // a := [3] int{}
   fmt.Println(a[0])
   fmt.Println(a[len(a)-1])
   //输出索引和元素
   for i, v := range a {
      fmt.Printf("%d %d\n", i, v)
   }
   //仅输出元素
   for _, v := range a {
      fmt.Printf("%d\n", v)
   }

}

初始化的时候默认值是0

q := [...]int{1,2,3}
fmt.Printf("%T\n",q) // "[3]int"

如果是 [...] 则说明数组的长度由初始化数组的元素个数决定

q := [3] int{1,2,3}
q = [4] int{1,2,3,4}//出现编译错误

数组的长度在编译时就可以确定

array := [5] string{1: "b", 3: "d"}

初始化部分值

package main

import "fmt"

func main() {
   array := [5]int{1, 2, 3, 4, 5}
   for i, v := range array {
      fmt.Println("数组索引:%d, 对应值:%s\n", i, v)
   }
}

数组的遍历

var c [2][3]int
    for i := 0; i < 2; i++ {
        for j := 0; j < 3; j++ {
            c[i][j] = i + j
        }
    }
    fmt.Println("2d: ", c)

二维数组

切片

动态数据结构,长度可以动态增长。因为高效并且占用内存少,使用比较多 可以看成是对数组的一层简单的封装,因为每个切片的底层数据结构中,一定包含一个数组 有三个属性,指针、长度和容量,

  1. 容量实际上代表了它的底层数组的长度
  2. 长度指的是切片的长度

切片类似于一个窗口,通过窗口看东西,当我们用make函数或切片值字面量(比如[]int{1, 2, 3})初始化一个切片时,该窗口最左边的那个小格子总是会对应其底层数组中的第 1 个元素

但是切片代表的窗口无法向左拓展,也就是说,我们永远无法透过切片看到对应数组中最左边的那 3 个元素

有两种生成方式,其中一种是通过在原有数组的基础上生成切片,第二种是直接调用 make 函数生成

package main

import "fmt"

func main() {
    //基于数组生成
   array := [5]string{"a", "b", "c", "d", "e"}

   slice := array[2:3]
   slice1 := array[2:4]
   fmt.Println(slice)
   fmt.Println(slice1)
   
   slice2 := make([] string. 4, 8)
   fmt.Println(len(slice2), cap(slice2), slice2)
}

切片格式中,包含索引 start 但是不包含 end(start 和 end 都可以省略, start 省略默认 0, end 省略 默认数组长度, 都省略默认整个数组)

//基于 make 函数生成
//长度为5
s1 := make([]int, 5)
//长度为5,容量为8
s2 := make([]int, 5,8)

修改操作 对切片修改后,原数组也会被修改,也就是说基于数组的切片,使用的底层数组还是原来的数组,一旦修改切片的元素值,那么底层数组对应的值也会被修改 Append 会自动处理切片容量不足的扩容问题,只要新长度不会超过切片的原容量,那么使用append函数对其追加元素的时候就不会引起扩容。这只会使紧邻切片窗口右边的(底层数组中的)元素被新的元素替换掉。

//追加一个元素

slice2:=append(slice1,"f")

//多加多个元素

slice2:=append(slice1,"f","g")

//追加另一个切片

slice2:=append(slice1,slice...)

Append 函数总会返回新的切片,如果新切片的容量比原切片的容量更大,那么就说明底层数组也是新的

Map

动态数据结构,长度可以动态增长

func main(){
    m := make(map[string]int) //使用make创建一个空的map

    m["one"] = 1
    m["two"] = 2
    m["three"] = 3

    fmt.Println(m) //输出 map[three:3 two:2 one:1] (顺序在运行时可能不一样)
    fmt.Println(len(m)) //输出 3

    v := m["two"] //从map里取值
    fmt.Println(v) // 输出 2

    delete(m, "two")
    fmt.Println(m) //输出 map[three:3 one:1]

    m1 := map[string]int{"one": 1, "two": 2, "three": 3}
    fmt.Println(m1) //输出 map[two:2 three:3 one:1] (顺序在运行时可能不一样)

    for key, val := range m1{
        fmt.Printf("%s => %d \n", key, val)
        /*输出:(顺序在运行时可能不一样)
            three => 3
            one => 1
            two => 2*/
    }
}

结构体

结构体是聚合类型,他们的值由内存中的一组变量构成,结构体的元素可以是不同的数据类型 静态数据结构,长度固定

语句

//if 语句
if x % 2 == 0 {
    //...
}
//if - else
if x % 2 == 0 {
    //偶数...
} else {
    //奇数...
}

//多分支
if num < 0 {
    //负数
} else if num == 0 {
    //零
} else {
    //正数
}

//Switch
switch i {
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")
    case 4,5,6:
        fmt.Println("four, five, six")
    default:
        fmt.Println("invalid value!")
}

//经典的for语句 init; condition; post
for i := 0; i<10; i++{
     fmt.Println(i)
}

//精简的for语句 condition
i := 1
for i<10 {
    fmt.Println(i)
    i++
}

//死循环的for语句 相当于for(;;)
i :=1
for {
    if i>10 {
        break
    }
    i++
}

注意无论任何时候,你都不应该将一个控制结构(( if for switchselect )的左大括号放在下一行。如果这样做,将会在大括号的前方插入一个分号,这可能导致出现不想要的结果

内存分配

有两种,分别是 new 和 make

  1. New

将内存清零,而不是初始化内存

  1. Make

用于创建切片、map 和管道,并且返回类型 T 的一个被初始化了的实例。

函数

用于封装代码、分割功能、解耦逻辑,还可以化身为普通的值,在其他函数间传递、赋予变量、做类型判断和转换等等,就像切片和字典的值那样。 函数值可以由此成为能够被随意传播的独立逻辑组件(或者说功能模块)。 函数签名就是参数列表和结果列表的统称,只要参数列表和结果列表一样就可以说是一样的函数

使用

package main

import "fmt"

type Printer func(contents string) (n int, err error)

func printToStd(contents string) (bytesNum int, err error) {
return fmt.Println(contents)
}

func main() {
    var p Printer
    p = printToStd
    p("something")
}

错误

Error 接口

只有一个 Error 方法用来返回具体的错误信息

type error interface {

   Error() string

}
  1. 例子

这里面由于 a 无法转为数字,所以报错

func main() {

   i,err:=strconv.Atoi("a")
   // i,err := strconv.Atoi("10")
   if err!=nil {

      fmt.Println(err)

   }else {

      fmt.Println(i)

   }

}
  1. 工厂函数:

自己定义的函数返回错误信息给调用者

func add(a,b int) (int,error){

   if a<0 || b<0 {

      return 0,errors.New("a或者b不能为负数")

   }else {

      return a+b,nil

   }

}

这里使用 errors.New 这个工厂函数生成错误信息。它接收一个字符串参数,返回 error 接口 在调用的时候就可以直接使用

package main

import (
   "errors"
   "fmt"
)

func add(a, b int) (int, error) {

   if a < 0 || b < 0 {

      return 0, errors.New("a或者b不能为负数")

   } else {

      return a + b, nil

   }

}

func main() {
   sum, err := add(-1, 2)
   if err != nil {
      fmt.Println(err)
   } else {
      fmt.Println(sum)
   }
}
  1. 自定义 error

平时的错误信息只能传递一个字符串,如果想要携带更多信息(比如错误码信息),那么就需要自定义 error 其实就是先自定义一个新类型,比如结构体,然后让这个类型实现 error 接口

type commonError struct {

   errorCode int //错误码

   errorMsg string //错误信息

}

func (ce *commonError) Error() string{

   return ce.errorMsg

}
  1. 错误的嵌套
package main

import (
   "errors"
   "fmt"
)

type MyError struct {
   err error
   msg string
}

func (e *MyError) Error() string {

   return e.err.Error() + e.msg

}

func main() {

   //err是一个存在的错误,可以从另外一个函数返回

   e := errors.New("原始错误e")

   w := fmt.Errorf("Wrap了一个错误:%w", e)

   fmt.Println(w)

}

结果

GOROOT=C:\Program Files\Go #gosetup
GOPATH=C:\Program Files\Go;C:\Program Files\Go\bin #gosetup
"C:\Program Files\Go\bin\go.exe" build -o C:\Users\Administrator\AppData\Local\Temp\GoLand___1go_build_awesomeProject__1_.exe C:\Users\Administrator\GolandProjects\awesomeProject\test_09.go #gosetup
C:\Users\Administrator\AppData\Local\Temp\GoLand___1go_build_awesomeProject__1_.exe
Wrap了一个错误:原始错误e

Deferred

可以保证文件关闭后一定被执行,不管是自定义函数的出现异常还是错误

func ReadFile(filename string) ([]byte, error) {

   f, err := os.Open(filename)

   if err != nil {

      return nil, err

   }

   defer f.Close()

   //省略无关代码

   return readAll(f, n)

}

以上面的 ReadFile 函数为例,被 defer 修饰的 f.Close 方法延迟执行,也就是说会先执行 readAll(f, n),然后在整个 ReadFile 函数 return 之前执行 f.Close 方法。

Panic

解决运行时的问题

func connectMySQL(ip,username,password string){

   if ip =="" {

      panic("ip不能为空")

   }

   //省略其他代码

}

如果是不影响程序运行的错误,不要使用 panic,使用普通错误 error 即可 场景:协程A执行过程中需要创建子协程A1、A2、A3...An,协程A创建完子协程后就等待子协程退出。 针对这种场景,GO提供了三种解决方案:

  • Channel: 使用channel控制子协程
  • WaitGroup : 使用信号量机制控制子协程
  • Context: 使用上下文控制子协程

一个程序启动,就会有对应的进程被创建,同时进程也会移动一个线程,这个线程叫做主线程。如果主线程结束,那么整个程序就退出了。有了主线程,就可以在主线程程里启动很多其他线程,就有了多线程的并发 但是 Go 里面没有线程的概念,只有协程,相对于线程更加轻量,一个程序可以随意启动成千上万个协程,而协程是被 Go runtime 调度, Go 自己决定同时执行多少个协程,什么时候执行哪几个 Go 语言中提倡通过通信来共享内存,而不是通过共享内存来通信,其实就是体长通过 channel 发送接收消息的方式进行数据传递,而不是通过修改同一个变量。