0%

Go

Go学习

面经

基本数据类型

bool,string,int,byte(int8),int16,rune(int32),float32,float64,complex64/128

Go比起Java和C++ ,他与C的关系更密切

变量:

//声明多个变量
var (
	v1
    v2
)
//变量初始化,无需声明变量类型
v1 := 1
//多重赋值
v1 , v2 = v2 , v1 //实现交换两个变量,但是只能交换两个同一类型的变量

^75e37b

类型:

float32 //等于c中的float
float64 // double
//复数类型,和数学上的表达一样
var (
		v1 = 1 + 5i
		v2 = -6i
	)
	fmt.Println(v1 + v2)
//输出为
(1-1i)
//中文字符在UTF-8中占三个字节
s := "你好"
fmt.Println(len(s)) 
//数组通过range来遍历,range有两个返回值所以,第一个返回的是数组的下标,第二个是返回的数组的数值,所以要用两个变量来存储
s := "0123456"
	for i, value := range s {
		fmt.Println(i, value-'0')
	}
// 单个字符
var b byte = 'a'
注意:禁止字符串转int
rune 是单个Unicode 字符
//同时注意数组的初始化的方式是这这样的
s := [7]int{0, 1, 2, 3, 4, 5, 6} 
//当不需要某个返回类型时使用_来跳过这个返回类型即可
for _, value := range s {
		fmt.Println(value)
}

类似 type of的方法

type 别名 = 原来的类型名
const 常量声明

itoa 用于记录常量

字符转换

strconv.xxxx即可

数组

数组定于的一种方式,会根据给定的初始化的元素决定数组的长度

a := [...]int{1}

数组切片,类似于vector

切片创建方式有:

  1. 基于原有的数组来创建

    a := [7]int{0, 1, 2, 3, 4, 5, 6}
    var s []int = a[:]
    //需要使用Var 来声明切片数组的类型,等号的右侧可以时 array[first , end] ,想要截取的首位位置
  2. 直接创建

    //使用make进行直接创建
    s := make([]int , 5 , 10)//建立一个初始原为5个但是预留十个元素的空间的切片
    //直接在建立的过程中赋予初值
    s := []int{1, 2, 3, 4, 5}

切片的部分常用函数

  • len() 返回已经存储的元素的个数
  • cap() 返回切片分配的空间大小
  • append() 追加元素
  • copy() 复制

数组也可以定为接口数组,结构体数组,管道数组等等

//接口数组
var unknown [2] interface{}
var unknown [...] interface{123,"你好"}
//管道数组
var chanList = [2] chan int{}

定义空数组:

var a [0] int

a := [...]int{1, 2, 4, 5, 6}
fmt.Println(a)

map类型

Go中将其变为基本类型,可以直接使用,不需要引入库

声明:

var myMap map[键的类型]值的类型

创建:初始化创建时要注意在初始化的最后一个数据的后面加上逗号才代表初始化完成,否则会报错

//指定容量
myMap = make (map[], capacity)
//直接初始化
myMap = map[string]int{
		"1234": 1234, //一定要加上逗号
}
//直接声明+初始化一起
myMap := map[string]int{
		"1234": 1234,
}

删除

delete (myMap , key)

查找

value, ok := myMap["4"]
	if ok {
		fmt.Println(value, ok)
	}
	{
		fmt.Println("No")
	}		

流程

  1. if else 结构

    • if else 的括号必须在一行,否则编译失败,当出现else时,else 必须和 ‘’} ‘’在一行

      //正确
      if ok {
      		fmt.Println(value, ok)
      	} else {
      		fmt.Println("No")
      	}
      //错误
      if ok {
      		fmt.Println(value, ok)
      	}
      	else {
      		fmt.Println("No")
      	}
      //所以也推荐使用省略else的语句
      if ok {
      		fmt.Println(value, ok)
      	}
      	{
      		fmt.Println("No")
      	}		
    • 条件不需要使用括号

    • 花括号必须存在

  2. switch : 不需要显性的使用break来退出判断,默认的已经带有break了

    t := time.Now();
    switch {//用来替代if 语句,比起嵌套if更好判断代码
    case t.Hour() < 12 :
        xxxx;
    default :
        xxxx;
    }
  3. 循环

    for i:= ; i < n; i++{
                          
    }
    // while循环
    for a > 0 {
    
            fmt.Printf("a = %d\n", a)
    
            a--
    
       }

    更强大的break break后面可以加上标签,使用方法与goto一致

  4. 函数

    Go的函数拥有多重返回值,可以更方便的把函数的执行结果返回

    注意:小写字母开头的函数只有本包可见,而大写字母开头的函数才可以被其他包使用

    一般语法

    func 函数名 (参数)(返回列表){
        
    }

    不定参数 在写形参列表的时候使用…来省略参数即可达到不定参数的作用

    例:

    func name(a ...int) int {
    	return 1
    }
    func main() {
    	fmt.Println(name(1))
    	fmt.Println(name(1, 2, 3))
    }
    

    概念: 这种使用方式是语法糖,语法糖对语言的功能没有用影响但是可以方便使用,也能够增加程序的可读性,减少出错的机会

    这里的 … 相当于一个数组切片,等价于下面的语句

    func name(args []int) int {
    	return 1
    }
    func main() {
        //调用的时候必须使用这种方式进行调用了
    	fmt.Println(name([]int{1}))
    	fmt.Println(name([]int{1, 2, 3}))
    }
    

    匿名函数:Go支持随时随地定义匿名函数

    //在定义的时候进行调用
    func main() {
    	fmt.Println(func(a int) int {
    		return a
    	}(1))
    }
    //匿名函数赋给变量
    func main() {
    
    	f := func(a ...int) int {
    		return a[0] // 注意此处的a相当于 args [] 所以a不是int型不可以直接返回
    	}
    	fmt.Println(f(1, 2))
    }

    闭包

    闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。

    匿名函数是一类闭包

    • 包含自由变量的代码块,这些变量不在这个代码块内或者任何全局上下文中定义,而是在定义代码块中的环境中进行定义的。要执行的代码块为自由变量提供绑定的计算环境(作用域)
    • 闭包每次调用都是新的实例
    • 闭包中的变量会保存,即使调用结束也会保存,,可以保证闭包中的变量的安全性,不会被外部函数修改

    闭包可以获得所在函数内的作用域,但是外部不可修改闭包的数据

    package main
    
    func main() {
    	b := 0
        //闭包
    	f := func(b int) {
    		b++
    		println(b)
    	}
    	f(b)
    	f(b) //每一次调用都是新的实例
    	println(b)
    }
    

    返回函数的函数

    package main
    
    import "fmt"
    
    func main() {
    	f := Add1()
    
    	fmt.Println(f(1, 2))
    	//fmt.Println(Add1()(1, 2)) 另一种方式
    }
    //返回了一个 匿名函数
    func Add1() func(a, b int) int {
    	return func(a, b int) int {
    		return a + b
    	}
    }
    
  5. 错误处理

    error接口是内置的,里面只有一个方法:

    type error interface {  
        Error() string
    }

    defer

    类似于析构函数,遵循先进后出,用于函数执行之后进行一些资源释放的收尾工作

    使用语法:

    defer + 执行语句
    要执行语句很多时可以写一个匿名函数来进行处理
    defer func (){
        回收工作
    }() //调用

    panic() 和 recover()

    panic ( ) 用于立刻终止程序,但是defer不影响,会正常执行,panic可以接受任意类型的数据

    recover ( ) 用于终止错误处理流程,一般放在defer中来截取错误信息

面型对象编程(OOP , Object Oriented Programming)

  1. 类型系统

    Go可以给任意类型(包括内置类型,但不包括指针类型)添加相应的方法

    type Integer int
    //Integer 与int 并无区别,只是我们认为给他加上了一个自带的方法
    func (a Integer) less(b Integer) bool {
    	return a < b
    }
    func main() {
    	var a Integer = 5
    	defer fmt.Println("回收了")
    	if a.less(2) {
    		fmt.Print(a, "<", 2)
    	} else {
    		defer fmt.Println("错误回收")
    	}
    
    }
    
  2. 面向对象只是换了一种方式来表达语法,所以实质上也是语法糖

    Go没有隐藏的指针

    • 方法的对象显式传递
    • 方法的对象不需要是指针,也不需要是this
  3. 对于public 类, 需要采用类名首字母大写

    package main
    
    import "fmt"
    
    type Base struct {
    	Name string
    }
    
    func (base *Base) Foo() {
    	fmt.Println(1)
    }
    func (base *Base) Bar() {}
    
    type Foo struct {
    	Base
    }
    
    func (foo *Foo) Foo(name string) {
    	foo.Name = name
    }
    func (foo *Foo) Bar() {
    	println(foo.Name)
    }
    func main() {
    	base := new(Base)
    	base.Foo()
    	foo := new(Foo)
    	foo.Foo("123")
    	fmt.Println(foo.Name)
    }
    
  4. 接口

    其他语言在使用接口时必须要先从接口进行继承,才能进行实现接口

    简言之,必须知道接口要实现什么才能定义接口,但实际情况是,不知道接口要实现什么

    例如

    //	Java 语法
    interface IFoo{
        void Bar();
    }
    class Foo implements IFoo {
        //....
    }

    问题:

    1. 提供哪些接口好呢?
    2. 如果两个类实现了相同的接口,应该把接口放进哪个包内?

    Go的接口是非侵入式的,只要类实现了接口要求的所有函数,就算是实现了这个接口,可以直接进行赋值

    package main
    
    import "fmt"
    //实现接口的类
    type File struct {
    }
    
    // 实现类方法,也就是接口的方法
    func (f *File) Print(str string) {
    	fmt.Println(str)
    }
    //两个以后出现的接口
    type IFile interface {
    	Print(str string)
    }
    type IPrint interface {
    	Print(str string)
    }
    
    func main() {
    	var file1 IFile = new(File)
    	var file2 IPrint = new(File)
    	file3 := new(File)
    	file1.Print("IFile接口实现")
    	file2.Print("IPrint接口实现")
    	file3.Print("实现接口的类,并不需要提前知道有哪些接口,只要能够实现了以后会出现的接口的函数即可直接使用")
    
    }
    

    但是 Go 语言里有非常灵活的 接口 概念,通过它可以实现很多面向对象的特性。接口提供了一种方式来 说明 对象的行为:如果谁能搞定这件事,它就可以用在这儿。

    接口定义了一组方法(方法集),但是这些方法不包含(实现)代码:它们没有被实现(它们是抽象的)。接口里也不能包含变量。

    type Namer interface {
        Method1(param_list) return_type
        Method2(param_list) return_type
        ...
    }

    使用接口时,如果某个类实现了接口的方法,那么接口类型的变量可以赋值为这个类的变量

    如果接口接口A和接口B的方法是一致的,那么在Go中属于完全等价

    如果A接口的方法是B接口方法的子集,那么B接口可以赋值给A接口,而A接口不能赋值给B接口

    接口查询:用于查询某个接口是否属于某个类型

    实例 num2 是否属于接口 Number1,可以这么做:

    var num1 Number = 1;
    var num2 Number2 = &num1;
    if num3, ok := num2.(Number1); ok {
        fmt.Println(num3.Equal(1))
    }

    如果num2 实例所指的对象是属于Number1的,那么ok值为1 ,num3 转化为 Number1的一个实例,之后执行条件内的代码

    类型查询:用于查询接口指向的对象实例的类型

    语法:

    switch v := v1.(type){ 	
    	case int:
        case string:
        ....
    }

    两者一般搭配使用

    Any类型

    任何对象实例都满足空接口 interface { } 所以interface { } 可以作为一个可以指向任何对象的Any类型,当函数可以接受任何实例时,可以将其声明为interface { }

    例如: fmt库中的Print函数

    func Print (fmt string , args ...interface{})

并发编程

主流实现模型:

  1. 多进程

    操作系统层面进行的

  2. 多线程

    操作系统之上的调度

  3. 基于回调的非阻塞/异步IO

    通过事件驱动的方式使用异步IO,使服务器持续运转并且尽可能的少用线程,降低开销,代表有Node.js,会对流程进行分割

  4. 协程

    本质是用户态线程,不需要抢占式调度,寄存于线程,缺点是需要语言支持

gorountine

通过在代码中加上go关键字启用协程,主程序结束时,写成会被结束

通信方式:消息机制和共享内存

消息机制:每个并发单位都是自包含、独立的个体,都有自己的变量,并且单元间的变量不共享,每个单元的输入输出只有一种那就是消息。不同进程依靠消息来进行通信,他们不会共享内存

channel

Go提供的goroutine间的通信方式,是进程内的通信方式,不适合进程之间进行通信

一个channel 只能传递一种类型的值

基本语法

//声明
var chanName chan ElemenType 
//例如 声明一个传递类型为int的channel
var ch chan int 
//定义
ch := make(chan int)
ch := make(chan int , 2) //缓冲大小为2
//写入channel
ch <- value //写入数据会导致陈孤虚阻塞,直到有其他goroutine 从channel中读取数据
//读取
value := <- ch // 如果channel中没有数据也会进行堵塞

示例:

package main

import "fmt"

func Count(ch chan int) {
	fmt.Println("Counting")
	ch <- 1 //写入数据,导致这个goroutine阻塞,直到有其他goroutine从中读取数据
}
func main() {
	chs := make([]chan int, 10) // 储存十个goroutine的channel数组
	for i := 0; i < 10; i++ {
		chs[i] = make(chan int)
		//启动十个协程
		go Count(chs[i])
	}
	for _, ch := range chs {
		<-ch //读取channel
	}
}

select

文件发生IO动作,select会被调用并返回

语法: 条件必须是IO操作

select {
    case <- chan1 : //从chan1成功读取到数据
    case chan2 <- 1 : //成功向chan2写入数据
    default
}

示例:

package main

import "fmt"

func main() {
	ch := make(chan int, 1)
	for j := 0; j < 3; j++ {
		//随机向ch 中写入一个1或者是0
		select {
		case ch <- 0:
		case ch <- 1:
		}
		i := <-ch
		fmt.Println("Value Received :", i)
	}
}

缓冲机制

建立channel数组即可实现缓冲

ch := make (chan int , 1024)

超时机制

Go中没有提供查实处理机制,但是可以使用select来处理,因为select只要有一个case已经完成即可继续进行下去

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int) //未写入任何数据,所以会把程序堵塞
	timeout := make(chan bool, 1)
	go func() {
		//等待一秒钟
		time.Sleep(1e9)
		timeout <- true
	}()
	select {
	case <-ch:
		fmt.Println("读取中")
	case <-timeout:
		fmt.Println("超时了")
	}
}

channel 传递,可以用来实现pipe 待补充

单向channel 用于防止写入某些不需要的数据或者被不需要的函数读取

只能读或者只能写

定义

var ch1 chan int //正常的channel
var ch2 chan <- float64 // 只能写
var ch3 <- chan int //只能读
//初始化
ch4 := make (chan int) // 正常
ch5 :=  <- chan int (ch4) // 从正常的channel进行类型转化为单项读取的channel
ch6 :=  chan <- int (ch4) // 转化为单项写入的channel

关闭channel

使用close()即可

如何判断已经关闭?

多重返回值的方式进行帕努的那

x , ok := <- ch
//只需要看ok即可,如果ch已经关闭,那么读取失败,ok的返回值为false 

同步问题

同步锁

sync包中提供

Mutex锁

当一个goroutine 获得了Mutex 后,其他的goroutine 只能等待这个释放锁

RWMutex 锁

单写多读 , 在读锁占用时,会阻止写,但是不会阻止读

全局唯一性操作

使用Once类型,当别的goroutine

网络编程

Socket编程 IP 层

socket是什么?

Socket是对TCP/IP协议的封装,自身并非协议而是一套调用的接口规范(API)。通过套接字Socket,才能使用TCP/IP协议。

传统的Socket编程主要步骤:

  1. 建立Socket: 使用socket() 函数
  2. 绑定Socket :使用bind() 函数
  3. 监听: 使用listen() 函数,或者connec() 函数
  4. 接受连接: accept() 函数
  5. 接收: receive() 函数 ,发送send() 函数

Go的:

Go语言标准库对此过程进行了抽象和封装,无论我们期望使用什么形式的连接,只需要调用net.Dial()即可

Dial原型

func Dial ( net , addr string ) ( Conn , error) 
/*
net : 网路协议名称
addr IP地址或域名,端口以":" 的形式跟在后面,端口号可选
Conn 是否成功连接
*/

工程管理

代码格式化

格式化之前的:

package main

import "fmt"

func Foo ( a , b int)(ret int ,err error){
if a>b {
return a , nil	
}else {
	return b , nil
}
return 0,nil
}
func 
main()  {
	i , _ := Foo(1,2)
fmt.Println("Hello,word" ,i)	
}

使用命令行: go fmt xxx.go得到的代码,也可以直接go fmt 会格式化所有*.go的文件

package main

import "fmt"

func Foo(a, b int) (ret int, err error) {
	if a > b {
		return a, nil
	} else {
		return b, nil
	}
	return 0, nil
}
func main() {
	i, _ := Foo(1, 2)
	fmt.Println("Hello,word", i)
}

gotool

生成exe

gobuild 后面可以加上-o + name 指定执行文件的名字

格式化输出: Printf

%v	按值的本来值输出
%+v	在 %v 基础上,对结构体字段名和值进行展开

生成随机数需要设置随机数种子

func main() {
	v := 100
	rand.Seed(time.Now().UnixNano())
	a := rand.Intn(v)
	fmt.Println(a)
}

依赖管理

Gin