GO语言基础整理

139 阅读10分钟

Go语言标准库文档中文版

Go语言圣经 - Go语言圣经 (gopl-zh.github.io)

指令/环境/目录结构

环境配置

名称含义
GOPATHgo项目目录/work/project/goproject
GOROOTgo安装目录/opt/go
GOOS指定编译后的文件运行的oslinux windows

指令

go build xxx.go      # 编译并生成可执行文件
go run xxx.go        # 编译运行某个程序
​

目录

目录结构如下:

bin

pkg

src 表示源码

src/project1 代表项目名称 src/project1/main 代表包名

包内的 main.go中必须指定 package为main

image-20230615144831140

命名规范

  1. 文件名: 下划线+小写
  2. 结构体/函数名:大驼峰。
  3. 对象/变量名: 小驼峰。
  4. 成员变量/方法: 大驼峰或小驼峰。
  5. 接口名: 接口名应该是描述性的名词,并以 “-er” 结尾。例如,Reader、Writer、Formatter、CloseNotifier 等。
  6. 常量名:大写+下划线。
  7. 包名=文件名

语法

基本数据

声明

var s,o string
var a="ok"
var x int = 5/*-------------------------------*
   使用:时相当于自带 var 和 type
   :=仅能用于局部变量的声明,全局变量不能用:=
 *--------------------------------*/
b:=2
// 综上所述,声明变量有如下几种:
var a int  //a=0
var a = 2
var a int = 2
a := 2// 以下几个声明后默认为nil
var a chan int
var a func(string) int
var a error // error 是接口var a,b *int//var一次声明几个变量
var (
    a int
    b string
    c []float32
    d func() bool
    e struct {
        x int
    } 
)
​
//匿名变量
func GetData() (int, int) {
    return 100, 200
}
func main(){
    a, _ := GetData()
    _, b := GetData()
    fmt.Println(a, b)
}
​
​

指针

// 指针的基础知识跳过
p := new(int)   // p, *int 类型, 指向匿名的 int 变量
fmt.Println(*p) // "0"
*p = 2          // 设置 int 匿名变量的值为 2
fmt.Println(*p) // "2"

const

const变量必须是编译器就能确定值的

//type可以不写
const identifier [type] = value
const (
    a = "abc"
    b = len(a)
    c = unsafe.Sizeof(a)
)
​
/*
   iota是go的枚举生成器
   在第一行引用 iota时,为0
   之后每定义一行const变量,iota自增1,并赋值给常量
*/
const (
    a = iota
    b 
    c
    d = 10*iota
    e 
) // 0,1,2,30,40
​

别名

type ttttt=uint8
// 从此以后多了一个类型 ttttt,定义byte就相当于定义uint8

基础变量

  1. bool:布尔类型,只能取 true 或 false。
  2. intint8int16int32int64:整型类型,分别表示不同位数的整数。其中 int 的大小是和平台相关的,通常为 32 位或 64 位。
  3. uintuint8uint16uint32uint64:无符号整型类型,分别表示不同位数的无符号整数。
  4. float32float64:浮点型类型,分别表示单精度浮点数和双精度浮点数。
  5. complex64complex128:复数类型,用于表示实部和虚部均为浮点数的复数。
  6. byte:= uint8,常用于表示 ASCII 码字符。
  7. rune:= int32,常用于表示 Unicode 字符。
  8. string:字符串类型,用于表示文本字符串。

rune和string

在go中,一个字符用rune标识,rune==int32始终为4字节。 因为unicode/utf8为了标识世界上的全部符号,最大就是4字节。

go字符串内部不是rune而是一系列字节。

如果想用下标去定位字符,可以用[]rune

rune

//通过 `去定义多行字符串
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"
r := []rune(s)
for i,v :=range r{
   fmt.Printf("%c%c",v,r[i])    
}

string

// 声明
s :="hello, world"
// 截取和连接
s1:=s[0:2]+"a"// 字符串面值,通过` `定义
const GoUsage = `Go is a tool for managing Go source code.
​
Usage:
    go command [arguments]
...`

在执行s+= ...时会对原字符串进行拷贝

在执行s[a:b]时不会进行拷贝,字符串会尽量共享内存

遍历字符串时:

   for i, r := range "Hello, 世界" {
        fmt.Printf("%d\t%q\t%d\n", i, r, r)
    }
0       'H'     72
1       'e'     101
2       'l'     108
3       'l'     108
4       'o'     111
5       ','     44
6       ' '     32
7       '世'    19990
10      '界'    30028

可以看到i并不是+1的,而是根据字符在String中的下标更新的,因为在执行 range时自动完成了utf8的转码

for i := 0; i < len(ste); i++ {
    fmt.Printf("ascii: %c  %d\n", str[i], str[i])
}

此时程序因为解码失败会报错

变量转化

todo

// 数字->字符串
x:= 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"
​
​

复合数据

数组

var a [3]int           
for i, v := range a {
    fmt.Printf("%d %d\n", i, v)
}
​
q := [...]int{1, 2, 3}        // 让编译器自动确定数组大小
​
q:=[100]string{0:"$",1:"%"}   // 根据下标定义值

切片

切片与数组在声明上仅有长度的区别,声明切片时不指定长度

s := []int{0, 1, 2, 3, 4, 5}

s1=s
s1=s[:]

make([]T, len)
make([]T, len, cap) 

append

func appendInt(x []int, y int) []int {
    var z []int
    zlen := len(x) + 1
    if zlen <= cap(x) {
        // There is room to grow.  Extend the slice.
        z = x[:zlen]
    } else {
        // There is insufficient space.  Allocate a new array.
        // Grow by doubling, for amortized linear complexity.
        zcap := zlen
        if zcap < 2*len(x) {
            zcap = 2 * len(x)
        }
        z = make([]int, zlen, zcap)
        copy(z, x) // a built-in function; see text
    }
    z[len(x)] = y
    return z
}
​

Map

go语言底层Map采用哈希表结构

map[xxx]int在key不存在时返回0(与cpp同)

ages := make(map[string]int)
ages := map[string]int{
    "alice":   31,
    "charlie": 34,
}
​
delete(ages, "alice") 
​
​
for k, v := range ages {
}
age, ok := ages["bob"]
if !ok {  // 没有这个key
​
}

流程控制

if

if i < 1{
   
}

// 可以在if前简单的执行一句
if i:=math.Abs(-1); i>=0{
   
}

for

// 1.无内容for
sum := 0
for {
    sum++
    if sum > 100 {
        break
    }
}


// 2.仅判断条件for
var i int
for i <= 10 {
    i++
}

// 3.完整 for
for i:=0;i<5;i++{
   ...
}


//4. range for, val为元素的副本
for key, val := range map {
    ...
}
for key :=range map{
   
}

switch

//switch无需break
var a = "hello"
switch a {
case "hello":
    fmt.Println(1)
case "world":
    fmt.Println(2)
case "a","b":
		...
default:
    fmt.Println(0)
}

//switch前也可以执行一个简单的语句
switch a:="Hello"; a{
	...
}


var r int = 11
switch {
case r > 10 && r < 20:
    fmt.Println(r)
}

var s = "hello"
switch {
case s == "hello":
    fmt.Println("hello")
    fallthrough   // 执行完这个case后,继续向下执行
case s != "world":
    fmt.Println("world")
}

defer

// defer后的语句会被推迟到函数结束时执行,一般用于文件资源释放等
func test(){
defer fmt.Println("world")
fmt.Println("hello")   
}

goto

err := firstCheckError()
if err != nil {
   goto onExit
}

err = secondCheckError()

if err != nil {
   goto onExit
}

fmt.Println("done")

return

onExit:
    fmt.Println(err)
    exitProcess()

函数

函数声明

// 只有一个返回值时无需加括号
// 有结构体名时代表为某个类写方法
func [结构体名] 函数名 (参数列表) [返回值|(返回值列表)]{
    函数体
}

// 几个相同类型的变量可以只写一个类型
func hypot(x, y float64) float64 {
    return math.Sqrt(x*x + y*y)
}

// 函数同样可以是一个变量
var a func(string) int
func fire(x string) {
    fmt.Println(x)
	 return 2
}

func main(){
   a=fire
   a("abc")
}

作用域和变量逃逸

var global *int
func f() {
    var x int
    x = 1
    global = &x
}
func g() {
    y := new(int)
    *y = 1
}

在上例中,变量x就逃逸了,编译器可以分析出结果,并自动将x创建在堆内存

y没有逃逸,虽然他用new关键字声明,但还是会被回收

匿名函数

// 在声明时就调用的
func(data int) {
    fmt.Println("hello", data)
}(100)

// 赋值给某个变量的
var f func(int) = func(data int) {
    fmt.Println("hello", data)
}
f(100)

闭包

简单来看,闭包=静态(编译时确定的)大函数A中的动态(运行时确定)小函数B,其中B引用了A中的局部变量。

package main

import (
    "fmt"
)

// 提供一个值, 每次调用函数会指定对值进行累加
func Accumulate(value int) func() int {
    // 返回一个闭包
    return func() int {
        value++
        return value
    }
}
func main() {
    accumulator := Accumulate(1)
    // 累加1并打印
    fmt.Println(accumulator())
    fmt.Println(accumulator())
    fmt.Printf("%p\n", &accumulator)
    // 创建一个累加器, 初始值为1
    accumulator2 := Accumulate(10)
    // 累加1并打印
    fmt.Println(accumulator2())
    // 打印累加器的函数地址
    fmt.Printf("%p\n", &accumulator2)
} // 2 3 11

闭包=函数体和引用环境,函数体定义了闭包的行为,而引用环境则提供了函数体所需要的外部变量。

如果你把闭包返回出去了,那么该闭包内引用到的局部变量都算逃逸,会声明在堆内存

可变参数

可变参数就是长度不定的参数,当作数组一样处理即可

func myfunc(args ...int) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

结构体/对象

type 类型名 struct {
    字段1 字段1类型
    字段2 字段2类型
    …
}
type Point struct {
    X int
    Y int
}

//声明
p:=new(Point) //返回指针
p:=&Point{}	// 与上同
p:=Point{}	// 返回结构体对象
var p Point	// 与上同
p:=Point{1,2}
 
p := Point{
    X: 1,
    Y: 2
}

方法

我们一般用一个特殊的函数来给类添加方法:

package main
​
import "fmt"// 车轮
type Person struct {
   Age int
}
​
func (p Person) say() {
   fmt.Println(p.Age)
}
​
func main() {
   p := Person{Age: 2}
   p.say()
}
​
​
// 需要注意的是,每次执行p.say()都会拷贝一遍p,因此我们最好写成这样:
// 这样就是对p本身进行操作
func (p *Person) say() {
   fmt.Println(p.Age)
}
//在调用时
p.say()

继承

在结构体中直接写另一个结构体就是继承

随后,内部结构体的所有方法/变量都会被外部结构体具有

package main
import "fmt"
type A struct {
    ax, ay int
}
type B struct {
    A
    bx, by float32
}
func main() {
    b := B{A{1, 2}, 3.0, 4.0}
    fmt.Println(b.ax, b.ay, b.bx, b.by)
    fmt.Println(b.A)
}

构造函数

在go中,没有构造函数的概念,如果要构造函数,请手动调用:

type Person struct {
    Name string
    Age  int
}

func NewPerson(name string, age int) *Person {
    return &Person{Name: name, Age: age}
}

func main(){
   p1:=NewPerson("Java",30)
}

析构函数

todo

func SetFinalizer(x, f interface{})

创建结构体

type Point struct {
    X, Y int
}

type Circle struct {
    Point
    Radius int
}

type Wheel struct {
    Circle
    Spokes int
}
p:=Point{1,2}
p:=Point{X:1,Y:2}
w = Wheel{Circle{Point{8, 8}, 5}, 20}

w = Wheel{
    Circle: Circle{
        Point:  Point{X: 8, Y: 8},
        Radius: 5,
    },
    Spokes: 20
}

导入包

每个包是由一个全局唯一的字符串所标识的导入路径定位。出现在import语句中的导入路径也是字符串。

import (
    "fmt"
    "math/rand"
    "encoding/json"

    "golang.org/x/net/html"

    "github.com/go-sql-driver/mysql"
)

包名,文件名

  1. 包名必须与最后一级目录名相同
  2. 一个包内的若干文件,都会通过包名被引用
// a.go
package test
func A(){
​
}
​
// b.go
package test
func B(){
​
}
​
​
​
// c.go: c.go是另外的一个包
package main
import "test"
func main(){
test.A()
test.B()
}

包名冲突

包名冲突一般发生在最后一级包名相同的情况

import (
    "crypto/rand"
    mrand "math/rand" // alternative name mrand avoids conflict
)

SDK

fmt

package main
​
import (
    "fmt"
)
​
func main() {
   // %d 表示整型数字,%s 表示字符串
    var stockcode=123
    var enddate="2020-12-31"
    var url="Code=%d&endDate=%s"
    var target_url=fmt.Sprintf(url,stockcode,enddate)
    fmt.Println(target_url)
}

input

容器

数组

声明数组时,必须指定大小,这是数组和切片声明的区别

var 数组变量名 [元素数量]Type
var a [3]int             
var a [3]int =[3]int{1,2}
a:=[3]int{1,2,3}
​
​
var a [4][2]int
var a =[2][2]int{{1,1,},{1,1}}
​
​
// i 为index,v为值
for i, v := range a {
      fmt.Println(i, v)
}
​
//数组可以用==和!=判断是否相等
// 程序会检查每一项是否相等
a==b,a!=b
​
// 若ba形状相同,则将b拷贝给a
a=b

切片

go中的切片与Python大致相同:

  1. go的切片只支持 [start:end]
  2. go的切片底层与数组共享内存
  3. go中切片和数组本身就是不同的数据类型,不能直接=赋值
  4. go中的切片在声明时,不能也不需要指定大小
// 声明
var a []int
var a []int = []int{1,2,3,4,5}
a:=[]int{1,2,3,4,5}
a:=b[1:2]
a := make([]int, 5)    //  len(a)=5
b := make([]int, 0, 5) // len(b)=0, cap(b)=5

// 操作
// 在执行append后切片可能发生扩容,此时会返回新切片
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包

/* 数组拷贝
	copy(dest,src []T) int
	拷贝最小长度并返回该长度
*/
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
len:=copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
len=copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置

map

var mapname map[keytype]valuetype
var mp map[string]int{
   "a":1,
   "b":2,
   "c":3,
}

mp["avb"]=2
delete(mp,"avb")
// ok为true时代表存在,反之不存在
// elem在不存在时为默认值
elem, ok = mp[key] 

文件io

Go语言数据I/O对象及操作 (biancheng.net)

package main

import (
	"fmt"
	"io"
	"strings"
)

func main() {
	r := strings.NewReader("Hello, Reader!")

	b := make([]byte, 8)
	for {
		n, err := r.Read(b)
		fmt.Printf("n = %v err = %v b = %v\n", n, err, b)
		fmt.Printf("b[:n] = %q\n", b[:n])
		if err == io.EOF {
			break
		}
	}
}

高级

错误

异常

defer

todo

接口

go程

goroutine是一种轻量级的线程实现方式,goroutine具有更小的内存占用和创建开销。

func main() {
    go sayHello()
    fmt.Println("Main function")
}
​
func sayHello() {
    fmt.Println("Hello, goroutine!")
}

信道

信道 chan是go中的通信机制

使用chan时必须使用符号 <-

非缓冲信道

package main
​
import "fmt"func sum(s []int, c chan int) {
   sum := 0
   for _, v := range s {
      sum += v
   }
   c <- sum // 将和送入 c
}
​
func main() {
   s := []int{7, 2, 8, -9, 4, 0}
   var 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)
}
​

带缓冲的信道

package main

import( "fmt"
"time")

func main() {
	ch := make(chan int,2)
	ch <- 1
	ch <- 2
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

总结,管道有带缓冲和不带缓冲之分,在声明信道时,我们可以指定大小x,此时声明的是缓冲管道,不带大小时声明的是非缓冲管道

  1. 非缓冲管道:

    非缓冲管道在放入或取出数据时,是会进入阻塞的,例如: A向管道放了数据,只要该数据没有被取走,则A会一直sleep. B从管道取数据,只要管道一直为空,则B会一直等待。

    package main
    ​
    import "fmt"func sum(s []int, c chan int) {
       sum := 0
       for _, v := range s {
          sum += v
       }
       c <- sum // 将和送入 c
    }
    ​
    func main() {
       s := []int{7, 2, 8, -9, 4, 0}
       var 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)
    }
    

    在上面的代码中,go了两个sum进程,在执行c<-sum时,这两个进程都陷入sleep

  2. 缓冲管道:

    缓冲管道在放入数据时,若管道满,则陷入等待。在取数据时,若管道空,则陷入等待。

要注意区分非缓冲管道缓冲管道的区别。大小为1的缓冲管道!=非缓冲管道

close和range

发送方可以调用 close来告知接收方信道关闭了

接收方可以通过 v, ok := <-ch,若ok为false则代表信道关闭

另外, range chan 会不断取信道值,直到信道关闭

package main
​
import (
   "fmt"
)
​
func fibonacci(n int, c chan int) {
   x, y := 0, 1
   for i := 0; i < n; i++ {
      c <- x
      x, y = y, x+y
   }
   close(c)
}
​
func main() {
   c := make(chan int, 10)
   go fibonacci(cap(c), c)
   for i := range c {
      fmt.Println(i)
   }
}

select

select语句会从多个可执行go程中选一个执行,若没有能执行的则陷入等待。

package main
​
import "fmt"func fibonacci(c, quit chan int) {
   x, y := 0, 1
   for {
      select {
      case c <- x:
         x, y = y, x+y
      case <-quit:
         fmt.Println("quit")
         return
      }
   }
}
​
func main() {
   c := make(chan int)
   quit := make(chan int)
   go func() {
      for i := 0; i < 10; i++ {
         fmt.Println(<-c)
      }
      quit <- 0
   }()
   fibonacci(c, quit)
}

在上面的代码中,main go程会在进行 c<-x后陷入阻塞,直到 func go程 打印 <-c, 随后,在经历了10次放入..打印后,main go程最后一次放入了斐波那契数列的第11项的值,并计算出了12项,等待放入,但func go程执行了quit,导致 main go程退出。

default

当select中其他go程都没准备好时,会执行defautl的内容

package main
​
import (
   "fmt"
   "time"
)
​
func main() {
   tick := time.Tick(100 * time.Millisecond)
   boom := time.After(500 * time.Millisecond)
   for {
      select {
      case <-tick:
         fmt.Println("tick.")
      case <-boom:
         fmt.Println("BOOM!")
         return
      default:
         fmt.Println("    .")
         time.Sleep(50 * time.Millisecond)
      }
   }
}

互斥锁

当我们只需要互斥的访问临界区时,就需要加互斥锁,而无需使用信道

package main
​
import (
   "fmt"
   "sync"
   "time"
)
​
// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
   v   map[string]int
   mux sync.Mutex
}
​
// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
   c.mux.Lock()
   // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
   c.v[key]++
   c.mux.Unlock()
}
​
// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
   c.mux.Lock()
   // Lock 之后同一时刻只有一个 goroutine 能访问 c.v
   defer c.mux.Unlock()
   return c.v[key]
}
​
func main() {
   c := SafeCounter{v: make(map[string]int)}
   for i := 0; i < 1000; i++ {
      go c.Inc("somekey")
   }
​
   time.Sleep(time.Second)
   fmt.Println(c.Value("somekey"))
}
​

泛型

func Add[T 此处填写类型1|此处填写类型2](a T, b T) T {  
    return a + b
}