golang 入门极简教程

基本语法

常量

常量可以类比于java中的final变量,必须在初始化时复制,不可以修改,不可以使用 :=

package main

import "fmt"

//全局常量
const a = "hello"
const b = 1
func main() {
    //局部常量
    const c = true
    fmt.Println(a)
    test()
    fmt.Println(c)
}

func test(){
    fmt.Println(b)
}
变量

变量的声明方式有一下几种方式:

var a int

var b string

c := 12

var d = "hello"

可以使用var关键字或者使用 :=的方式声名并赋值

package basics

import "fmt"
//函数体外的全局变量要遵循闭包规则,不可以使用 := 或者 var c
var a string
var b int
var c [] int
var d [5] int
// e := 12  golang具有闭包规则。 := 其实是两步操作, var e int +  e = 12  而 e = 12 是不能在函数体外执行的
func test() {
    a = "hello"
    e := "world"
    var f = "just"
    
    fmt.Println(a)
    fmt.Println(e)
    fmt.Println(f)
}
基本数据类型
类型 描述
浮点型 float32,float64,complex32
整型 byte,int,int8,int16,int32,uint
布尔型 bool
字符串 “ ”里的字符串可以进行标量替换, ``里的字符串是什么就是什么

字符串支持切片

package main
import "fmt"
func main(){
    a := "hesitate"
    for _,char := range a{
        fmt.Printf("%c",char)
        fmt.Printf("--")
    }

    fmt.Println()

    fmt.Printf(a[2:])

    fmt.Println()

    b:= "hello"
    c:= "world"
    b+=c
    fmt.Printf(b)

    fmt.Println(len(a))

}
数组

数组是一个定长的序列,可以使用以下语法来创建

  1. [length]Type

  2. [length]Type{v1,v2,v3...}

  3. [...]Type{v1,v2,v3...}

    使用[...]系统会自动计算数组长度

切片

由于数组是定长的,实际使用中更多用到的还是切片,切片是可以调整长度的,切片的创建语法如下:

  • []Type{}
  • []Type{v1,v2,v3...}
  • make([]Type,length,capacity)
  • make([]Type,length)

切片的一些操作:

s1 := []int{1, 2, 3, 4, 5} 
s2 := make([]int, 2, 4)    //make语法声明 ,初始化len为2,cap为4
s2 = []int{5, 6}

s3 := append(s2, 7) //append一个元素
fmt.Println(s3, s2) //[5 6 7] [5 6]

s4 := append(s2, s1...) //append  一个切片所有的元素
fmt.Println(s4)         //[5 6 3 4]

    //return
copy(s1, s2)    //  复制,用s2的元素填充s1里去,改变原slice,覆盖对应的key
fmt.Println(s1) //[5 6 3 4]

s1[0], s1[1] = 1, 2
copy(s2, s1)
fmt.Println(s2) //[1 2] 目标slice len不够时,只填满len

s5 := s1[1:4]
s6 := s5[0:4] //不会报错,因为cap为4,从底层取得最后一位

fmt.Println(s5, s6, cap(s6)) //[2 3 4] [2 3 4 5] 4

//删除第三个元素
s7 := append(s1[:1], s1[3:]...)
fmt.Println(s7) //[1 4 5]

GOPATH

工程源文件存放于$GOPATH/src 目录下,不过gopath的方式慢慢被废弃,才是go module的方式

流程控制

switch

switch的表达式如下:

switch optionalStatement; optionalExpression {
    case expression1: block1
    ...
    case expressionN: blockN
    default: blockD
}

但是需要注意的是 Go 语言的 switch 语句不会自动贯穿,相反,如果想要贯穿需要添加 fallthrough 语句。

demo:

package main
import "fmt"
func main(){
    num := 3
    //switch不含expression
    switch  {
    case num > 0:
        fmt.Println("num is gt 0")

    case num > 1:
        fmt.Println("num is gt 1")

    case num < 0:
        fmt.Println("num is lt 0")  
    
    default :
        fmt.Println("wrong num")
    }
    fmt.Println("----------")
    //先执行前置运算,再赋值
    switch a:= get();a{
    case 1:
        fmt.Println("one")
    case 2:
        fmt.Println("two")
    case 3:
        fmt.Println("three")        
    }
    fmt.Println("----------")
    b := "i"
    //多情况匹配
    switch b{
    case "a","e","i","o":
        fmt.Println("this is vo")
    case "b":
        fmt.Println("this is b")    
    }
}

func get() int{
    return 3
}

if

if的写法如下:

if condition {  
} else if condition {
} else {
}

同switch一样,也有如下变体:这种形式的 if 语句先执行 statement,然后再判断 conditon

if statement; condition {  
}

上面的变体中的变量仅作用于该if语句。

函数

函数的定义包括: func关键字,函数名,参数,返回值,方法体,方法出口。

// Switch 函数包含 func 关键字, 方法名,参数,返回值,方法体,return
func Switch(a int)(b bool){
    switch a {
    case 1:
        return true
    case 2:
        return false
    default:
        return false
    }
}

同java不同的是,go的函数可以有多个返回值,具体如下:

func Divide(a int,b int)(c int,err error){
    if b==0{
        err = errors.New("被除数不能为0")
        return
    }
    return a/b,nil
}

// Add 返回值可以只写类型
func Add(a,b int)(int,string){
    return a+b,"success"
}

匿名函数:可以定义一个没有名字的函数,然后赋值给一个变量,或者直接运行:

//匿名函数
    f := func(a, b, c int) (result bool) {
        if a+b+c > 10 {
            result = true
        } else {
            result = false
        }
        return result
    }
    fmt.Println(f(1,2,3))
//也可以不定义变量,直接在结尾拼上参数直接运行该函数 
defer

defer可以使栈帧中的代码执行完后,以出栈的方式来执行。程序异常并不会影响defer运行,因此可以使用defer做资源的释放,或者是异常的捕获。

//测试一下defer函数

func Delay(){
    fmt.Println("start ...")
    defer foo1()
    defer foo2()
    defer foo3()
    fmt.Println("end ...")

}

func foo1(){
    fmt.Println("do 001...")
}

func foo2(){
    fmt.Println("do 002...")
}

func foo3(){
    fmt.Println("do 003...")
}

以上代码输出:

start ...
end ...
do 003...
do 002...
do 001...

类型断言

类型断言针对interface{}类型的变量,其使用方法有以下两种

  1. 安全断言:s,ok := s.(T)
  2. 非安全断言:s:=s.(T)

其中s表示interface{}类型的变量,ok为是否断言成功,T为断言的类型。

//interface{} 包含所有类型,类似于object
    var i interface{} = 99
    //类型断言
    j:=i.(int)
    fmt.Printf("type of j is %T,value is %d \n",j,j)
    var s interface{} = []string{"left","right"}
    if s,ok:=s.([]string);ok{
        fmt.Printf("s is a string slice,value is %s",s)
    }
异常

使用panic(interface{})来抛出异常,类似于java中的throw

使用recover()可以捕获异常,类似于java中的catch。


func foo(){
    panic(errors.New("i`m a bug"))
    return
}
func Wrong() int{
    defer func(){ //捕获异常
        if r:=recover();r!=nil{
            err:=r.(error)
            fmt.Println("catch an exception",err)
        }
    }()
    foo()
    return 10
}

面向对象编程

指针

go中的指针表示指向内存地址的一种数据。使用方法:

v:="cat"
ptr:=&v //使用&来获得v变量的指针
s:=*ptr //使用*来获得指针所指向的变量的值

&用于变量,来取得指针

*用于指针,来取得变量

func Point(){
    a:="nothing to fear"
    ptr:=&a  //&表示取得变量的内存地址
    //变量ptr为 *string 类型
    fmt.Printf("a的内存地址为%p \n",ptr)
    fmt.Printf("prt的类型为%T \n",ptr)
    //使用*来获得指针指向的内存值
    fmt.Println(*ptr)

}

也可以使用new关键字来创建一个指定类型的指针:

func Create(){
    //使用new来创建指定类型的指针
    ptr:=new(string)
    fmt.Println(ptr)
    *ptr = "range your dream"
    fmt.Println(*ptr)
}

当一个指针没有指向任何内存地址时,其值为nil

结构体

go中结构体为一系列数据类型(基本数据类型,interface,自定义结构体)的组合,eg:

type ColorPoint struct {
    color.Color  //匿名字段
    x,y int //具名字段
    r Print //interface类型
    m Man //自定义结构体
}

type Print interface {
    red() string
}
type Man struct {
    name string
    age int
}

接口同java中的接口类似,为一系列方法的集合。

方法

方法是特殊的函数,定义在某一特定的类型上,通过这个类型的实例(接收者)来调用。

go中方法不像其他语言一样,是定义在类中,而是通过接收者来关联。

接收者必须显式地拥有一个名字,在方法中必须被使用。

type Count int //go不允许为内置的数据类型添加方法,这里自定义了一个类型

func (c Count) Add(i Count){
    fmt.Printf("add result is : %d",c+i)
}

func (c *Count) Increment() {
    *c++
}

func (c *Count) IsZero() bool{
    return *c==0
}

方法相比于函数,在func关键字和方法名中间增加了接收者,接收者可以是指针类型,也可以是接收者本身

type Cat struct {
    Name string
}

func (c *Cat) Tell(){
    fmt.Println("喵喵")
}
func (c Cat) Catch(){
    fmt.Println("抓老鼠")
}
func (c *Cat) HisName() {
    fmt.Println(c.Name)
}
组合

go中可以使用组合的方式来实现继承

type Base struct {
    Name string
}

func (b *Base) Foo(){
    fmt.Println("foo...")
}
func (b *Base) Bar(){
    fmt.Println("bar...")
}

type Speed struct {
    Base
}

func (s *Speed) Foo(){
    fmt.Println("重写foo...")
}

func main(){
    s:=new(Speed)
    s.Base.Foo() //可以调用base中的所有方法
    s.Foo() //s对原方法进行了重写
}
接口

go中的接口定义了函数名,参数及返回类型,不进行函数的实现。以java为例,类实现接口是通过implements关键字,并对接口中的函数进行实现。在go中也是以结构体来对接口进行实现的,并不需要implements或者其他的关键字,而是只要实现了接口中定义的所有的函数,就表示这个结构体实现了接口。

type Sharp interface{
    Area() float64
    Perimeter() float64
}

type Rect struct {
    width float64
    height float64
}

func (r Rect) Area() float64{
    return r.width * r.height
}

func (r Rect) Perimeter() float64{
    return (r.width+r.height)*2
}
// Rect实现了Sharp接口

一个结构体也可以实现多接口:

type Sharp interface{
    Area() float64
    Perimeter() float64
}

type Detail interface {
    Desc()
}
type Rect struct {
    Width  float64
    Height float64
}

func (r Rect) Area() float64{
    return r.Width * r.Height
}

func (r Rect) Perimeter() float64{
    return (r.Width +r.Height)*2
}
// Rect实现了Detail接口
func (r Rect) Desc(){
    fmt.Println("this is a rect")
}
func main(){
    r:=Rect{Width: 10, Height: 20}
    var s Sharp = r
    var d Detail = r
    fmt.Printf("type of s is %T,type of d is %T \n",s,d)
    //类型断言
    sharp,ok := s.(Sharp)
    if ok {
        fmt.Println(sharp.Perimeter())
    }
}

使用接口可以达到多态的效果。

接口也可以嵌入到接口中,实现接口的组合:

type Interface1 interface {
    Send()
    Receive()
}

type Interface2 interface {
    Interface1
    Close()
}
//Interface2隐式地包含了Interface1
内存分配

go中的内存分配的关键字使用makenewmake用于对slice,map,channel分配内存

并发编程

协程

提到高并发的解决方案便会想到多线程,多线程其实是以获得cpu时间片的方式来解决单线程的阻塞等待的问题。显而易见如果一个任务持续需要cpu的计算能力,那么多线程上下文的切换反而会让效率变低。所以对于高密度计算的程序的线程数一般设置为cpu的核心数。而对于io密集型的程序,由于请求会阻塞到io,这时线程切换来处理其他请求,可以极大的提高系统的吞吐量。

但是系统内存资源有限,目前操作系统支持的最大线程数为千量级,那么可以使用比线程更细粒度的协程

协程是在用户态来实现的,所以和线程以及进程是两个层面的概念。操作系统无法感知协程,所以一个线程内的协程在某一时刻只会有一个在运行。但由于是在用户态,所以不再存在线程切换带来的时间损失。golang的并发正是基于协程来实现的。

goroutine

Go 程序中使用 go 关键字为一个函数创建一个goroutine。一个函数可以被创建多个 goroutine,一个 goroutine 必定对应一个函数。

func GoAdder(){
    var times int
    for true {
        times++
        fmt.Println("tick",times)
        time.Sleep(time.Second)
    }
}
func main(){
    go GoAdder()
    var string input
    fmt.Scanln(&input)
}
channel

我们无法直接取得goroutine函数的结果,可以通过channel

channelgoroutine 之间互相通讯的东西。channel 是类型相关的,也就是说一个 channel 只能传递一种类型的值,这个类型需要在 channel 声明时指定。

channel是一个FIFO的队列,在任何时候,同时只能有一个 goroutine 访问通道进行发送和获取数据

channel 的一般声明形式:var chanName chan ElementType

即在普通变量的基础上增加了chan这个关键字

声明的变量需要使用make分配内存空间后才能使用:

通道实例 := make(chan 数据类型)

channel的读写操作
c <- x        //向一个Channel发送一个值
<- c          //从一个Channel中接收一个值
x = <- c      //从Channel c接收一个值并将其存储到x中
x, ok = <- c  //从Channel接收一个值,如果channel关闭了或没有数据,那么ok将被置为false

默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的:

1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。

2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

func main(){
    m:=make(chan string)
    //开启一个routine做通道的发送方
    go func() {
        time.Sleep(time.Second*2)
        m <- "hello"
    }()
    //main routine做通道的接收方,该语句会阻塞直到接收到数据
    s:= <- m
    fmt.Println(s)
}
循环接收

通道中的数据每次只能接收一个,但是通道是可以遍历的,遍历的结果就是接收到的数据:

func main(){
    m:=make(chan int)
    go func() {
        for i:=3;i>=0;i-- {
            m <- i
        }
    }()

    for k:= range m {
        //打印通道数据
        fmt.Println(k)
        if k == 0 {
            break
        }
    }
}
deadLock

fatal error: all goroutines are asleep - deadlock!

学习channel时这个错误并不陌生,下面通过一段程序来分析一下:

func TestDeadLock(a chan int){
    fmt.Println( <- a)
}
func main(){
    m:=make(chan int)
    m<-0
    go base.TestDeadLock(m)
    time.Sleep(time.Second*2)
}

执行上面程序会出现deadlock的异常,我们分析在m<-0时,由于还没有接收者,程序在此阻塞,导致无法执行之后的代码,抛出deadlock异常。那么先调换一下顺序:

func TestDeadLock(a chan int){
    fmt.Println( <- a)
}
func main(){
    m:=make(chan int)
    go base.TestDeadLock(m)
    m<-0
    time.Sleep(time.Second*2)
}

发现程序正常执行。证实了非缓冲通道的阻塞性。

带缓冲的通道

如何创建带缓冲的通道呢?参见如下代码:

通道实例 := make(chan 通道类型, 缓冲大小)

  • 通道类型:和无缓冲通道用法一致,影响通道发送和接收的数据类型。
  • 缓冲大小:决定通道最多可以保存的元素数量。
  • 通道实例:被创建出的通道实例。
func ChannelBuffer() {
    //创建一个包含3个元素缓冲区的通道
    c:=make(chan int,3)
    //不会再有deadlock的异常
    c <- 0
    c <- 1
    c <- 2
    //查看当前通道的大小
    fmt.Println(len(c))
}

这就是我们常见的生产者/消费者模式了

channel的关闭使用内置的close(ch)函数

select

java nio中的核心实现在于Selector来实现多路复用,golang则是使用select关键字。


func GoSelector() {
    n:=time.Now()
    c1:=make(chan interface{})
    c2:=make(chan int)
    c3:=make(chan string)

    go func() {
        time.Sleep(time.Second*4)
        close(c1)
    }()

    go func() {
        time.Sleep(time.Second*3)
        c2 <- 1
    }()

    go func() {
        time.Sleep(time.Second*3)
        c3 <- "hello"
    }()

    fmt.Println("wait to read...")
    for {
        select {
        case <-c1:
            fmt.Printf("Unblocked %v later.\n", time.Since(n))
        case m2 := <-c2:
            fmt.Printf("read from c2:%d .\n", m2)
        case m3 := <-c3:
            fmt.Println("read from c3:", m3)
        default:
            fmt.Println("exec default ...")
        }
    }
}

你可能感兴趣的:(golang 入门极简教程)