Go语言立地入门

Go语言概述

Go语言又名Golang,由谷歌公司开发并推出,它的主要目标是"兼具Python等动态语言的开发速度和C/C++等编译型语言的性能和安全性"。从Go语言的语法中,我们可以看到C/C++、Python等优秀语言的影子,它借鉴了其它主流语言的优势(自动垃圾回收、切片、字典),并实现了自己的特点 (协程开发、动态接口)。

Go语言的主要特点有:

  • 自带编译器,可进行交叉编译,即在运行 Linux 系统的计算机上开发在 Windows 上运行的应用程序。
  • 自动垃圾回收机制 ,使用三色标记和写屏障等机制提高回收性能。
  • 优秀的原生并发编程机制,gorounting和channel为协程式编程提高了良好的支持。
  • 声明即用,声明的变量和引入的函数必须要进行使用,避免了无用代码的堆积。
  • 动态接口实现,遵循duck type原则,不用显式声明接口也可以进行多态使用。

变量和常量

Go语言的变量/常量声明很简单:

var a := 10

var为变量的修饰符,const为常量的修饰符。上面的例子就是声明了一个变量a,同时给它赋值为10。你也可以先进行声明,等到后面再赋值:

var a int
a = 10

注意:当你声明了一个变量,如果没有使用它,那么编译器会报错。当你想阻止这种行为时,可以这样做:

 _ = a

这段代码确实用到了a,只不过没有对a进行任何操作。

Go源码一般在用户工作区(即创建项目的目录),它包含三个子目录:

  • src目录:以代码包的形式保存源码文件,源码一般都放在这里。
  • pkg目录:存放通过go install目录安装后的代码包的归档文件。
  • bin目录:通过go install安装后,保存生成的可执行文件。

在src目录下导包时,要记得将工作区路径添加到GOPATH中,只有这样在编译时才能找到对应的代码包。

导包的语法为:

import "包名"

// 不想加前缀,而直接使用某个依赖包中的程序实体
import . "包名" 

import {
    "包名"
}

import {
    "别名" "包名"
}

Go的目录结构如图所示:1592551265957

在Go中,没有访问修饰符这种关键字,而是简单地通过标识符的首字母大小来控制程序实体的访问权限。如果标识符首字母大写,那么所对应的程序实体可以被本代码包之外的代码访问,然后不行。这个规则对于结构体、接口和函数都有效。

Go中每个包都有一个内置函数init(),当包初始化时,首先初始化所有的全局变量,然后会自动执行该函数的内容,相当于java的static静态代码块

类型

基本类型

Go中有很多预定义类型,可以将它们简单地划分为基本类型和高级类型,基本类型有:

类型宽度初始值
bool1false
byte10
rune40
int/uint-0
int8/uint810
int16/uint1620
int32/uint3240
int64/uint6480
float3240.0
float6480.0
complex6480.0 + 0.01
complex128160.0 + 0.01
string-“”

intXX/floatXX/complex表示不同进制位下的整数/浮点数 /复数类型。

int/uint的实际宽度是根据计算机架构而变化的,在x86-64下,宽度为8字节。

byte(可看做uint8)是代表了一个ASCII编码的字符,rune类型(可看做int32)是用于存储Unicode编码的字符,同时rune字面量还支持转义字符。

在Go语言中不支持类型的自动转换,如果需要强转需要显式声明,比如:

var num int16 = 32
var num2 int32 = int32(num)

Go语言提供了专门的包用于字符串与其它数据的相互转换:

var str string = strconv.Itoa(num) // 数字-->字符串 Itoa是Format
num2,_  := strconv.Atoi(str) // 字符串-->数字 Atoi是ParseInt

// 使用FormatXXX()将给定类型格式化为string类型
str = strconv.FormatBool(true)
str = strconv.FormatFloat(3.0, 'E', -1, 64)
str = strconv.FormatInt(-10, 16)
str = strconv.FormatUint(10, 16)

// 使用ParseXXX()将string类型转换为给定类型
pbool := strconv.ParseBool("true")
pint := strconv.ParseBool("123", 10, 32)
puint := strconv.ParseBool("123", 10, 32)
pfloat := strconv.ParseFloat("3.0", 32)

获取类型的方法可以通过反射:

num := float64(3.14)
println(reflect.TypeOf(num).Name())

也可以在后面章节里通过.(type)配合switch进行获取。

高级类型有:

数组

数组的声明方式为:

// 指定长度
var arr [4]int = [4]int{1, 2, 3, 4}

// 根据初始化值指定数组长度
arr := [...]int{1, 2, 3, 4}

我们可以通过==!=来比较两个元素类型相同的数组是否相同(数组长度、数组元素)。

数组的使用和在其它语言的使用一样,都是通过索引访问和修改元素。对于数组的遍历一般是使用for控制语句:

// 从原数组复制元素到新数组
arr2 := arr[2:]

// arr[i:]  从 i 切到最尾部
// arr[:j]  从最开头切到 j(不包含 j)
// arr[:] 

for idx, val := range arr{
    
}

for i := 0; i < len(arr2); i++  {
		
}

数组一旦声明后,长度不可修改,因此如果想动态修改数组长度只能通过创建一个新数组,将旧数组的数据复制到新数据。

切片

切片和数组很相似,都是基于索引访问元素的,但是它可以动态修改自己的长度,因此大多数情况下都是用切片。

切片的定义有:

var slice []type = make([]type, len) # len 为切片长度

slice :=[] int {1,2,3} 

通过len(数组)返回的是数组长度,通过len(切片)返回的是切片元素的个数,如果想知道切片容量大小,就应该用cap(切片)

可以使用slice = append(slice, el)向原来切片添加元素,在切片的容量小于 1000 个元素时,容量的增长因子会设为 2。一旦元素个数超过 1000,容量的增长因子会设为 1.25。

注意 :截取的切片和原切片共享同一段底层数组,如果一个切片修改了该底层数组的共享部分,另一个切片也能感知到。Go内置的copy(dst, src)函数可以将一个切片中的元素拷贝到另一个切片中,表示把切片 src 中的元素拷贝到切片dst 中,返回值为拷贝成功的元素个数。如果src比dst长,就截断;如果 src比dst 短,则只拷贝src那部分。

字典

字典类似于java的Map结构,通过key-value键值对存储元素。

字典的操作方法:

var map1 map[string]string // 声明一个字典

map1 = make(map[string]string) // 使用 make 函数对map初始化

map1["name"] = "zs" // 赋值操作
fmt.Println(map1["name"]) // 读取操作
delete(map1, "zs") // 删除操作
_,ok = map1["name"]// 如果name存在,ok=true,否则ok=false

// 字典遍历,在Go中没有类似keys()和values()这样的方法,如果我们想获取key和value需要自己循环
for key,val := range fruits{
    fmt.Println(key, val)
}

字典默认是无序的,如果要为map排序,需要将key/value拷贝至一个切片中,再对切片排序,然后再使用。

函数

函数作为Go的一等公民,能够当做值来传递和使用,它既可以作为其它函数的参数,也可以作为返回结果。

函数的简单使用:

func test1(){int, error}{
    var res int
    var err errror
    // doSomething
    return res, err
}

// 如果返回列表中指定了返回值的字面量,那么可以直接return
func test2(){res int, err error}{
    // doSomething
    res = ...
    err = ...
    return
}

将函数作为值的使用:

func test1(fun func()) func() {
	print("我是test1\n")
	fun() // 函数作为参数值
	return test2 // 函数作为返回值
}

func test2()  {
	print("我是test2\n")
}


func test3()  {
	print("我是test3\n")
}

func main() {
	t := test1
	t(test3)()
}

函数可以接收值和指针。对于非指针的数据类型,与它关联的方法的结合中只包含它的值方法,对于它的指针类型,其方法集合中既包含值方法也包含指针方法。Go在内部可以对值和指针进行转换,所以对于非指针数据类型的值,也可以调用其指针方法。

接口

Go的接口遵循Duck Type原则,并且不需要像java那样显式实现,只要Go中的结构体实现了某个接口的所有方法,那么它就自动与该接口绑定。接口是Go多态的一种实现机制。

接口的使用:

type Person interface {
	Say(msg string)
}

type Student struct {
	name string
	age int
}

func (this *Student)Say(msg string)  {
	fmt.Printf("学生 %s 说:%s\n", this.name, msg)
}

type Teacher struct {
	name string
	age int
}

func (this *Teacher)Say(msg string)  {
	fmt.Printf("老师 %s 说:%s\n", this.name, msg)
}

func main() {
	var person Person
	person = &Student{"zs", 12}
	person.Say("你好")
	person = &Teacher{"ls", 40}
	person.Say("好啊")
}

结构体

Go的结构体和java的对象概念差不多,包含属性和方法,但是在其它地方有很大不同。我们可以看看比较:

public class UserBean implements Serializable {
    private Integer id;
    private String name;

    public UserBean() {
    }

    public UserBean(Integer id, String name) {
        this.id = id;
        this.name = name;
    }

    public String getName() {
        return username;
    }

    public void setUName(String username) {
        this.username = username;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }


    @Override
    public String toString() {
        return "UserBean{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

type User struct {
	id int32
	name string
}

func (self *User) GetId() (int32) {
	return self.id
}

func (self *User) SetId(id int32)  {
	self.id = id
}

func (self *User) GetName() (string) {
	return self.name
}

func (self *User) SetName(name string)  {
	self.name = name
}

func (self *User) String() (string)  {
	return fmt.Sprintf("id = %d, name = %s", self.id, self.name)
}

前面我们说过,Go中的访问权限是由标识符首字母大小写决定的,在上面的go代码中将User结构体的字段定义为私有,将它的方法定义为公开,func (self *User)表示这是User的方法而不是函数。

跟Java中打印对象实际上是调用对象的toString()方法类似,Go也提供了对应的机制,当你使用fmt.Printf("%+v\n", obj)打印对象时,实际上调用了对象的String()方法:

// 在创建结构体时可以指定结构体字面量或者忽略:
user1 := User{12, "zs"} // 没有指定字面量时,传值的顺序要遵循结构体字面量的顺序
user2 := User{id:12, name:"ls"}
fmt.Printf("%+v\n", user1)
print(user2.String(), "\n")

控制流程

Go语言简化了控制流程语句的规模,并进行了一定的修改,主要特点有:

  • 没有do和while循环(它们的功能由for语句实现)。
  • 增强了switch功能。
  • 使用defer关键字实现了类似于java的finally功能。
  • 使用go语句开启goroutine多线程功能。
  • select语句与通道配合使用,增强对多线程的支持。

下面,我们来一一看看Go的流程控制语句。

if

这没什么好说的,主要注意在Go中if语句可以添加一个初始化语句,比如:

if val := test(100); val > 100{ // 当val条件为true时执行if代码块的内容
    
} else if val > 50 {
    
} else {
    
}

for

for语句一般有三种使用方式:

普通for

for i := 0; i < len(arr); i++ {
    
}

do/while

for m < 100 {
    m *= 2
}

range遍历

for idx, val := range arr {
    
}

注意,不同的类型使用range的迭代产出不同:

类型产出值1产出值2
数组索引元素值
字符串字符下标字符rune值
切片索引元素值
字典
通道元素值

switch

switch语句提供多分支执行的方法,一般情况下用于两种情况:

普通的分支执行:

switch con := test(); con {
	case 1:
		fmt.Printf("this is 1\n")
	case 2:
		fmt.Printf("this is 2\n")
	case 3:
		fmt.Printf("this is 3\n")
    // 默认情况下当匹配到一个case时,后面便不再匹配,但是可以用fallthrough指示继续向下匹配
		fallthrough 
	default:
		fmt.Printf("this is unkown\n")
}

匹配数据类型:

var num interface{}
num = "3"
switch num.(type) {
    case int:
    fmt.Printf("this is 1\n")
    case string:
    fmt.Printf("this is 2\n")
    case float32:
    fmt.Printf("this is 3\n")
    default:
    fmt.Printf("this is unkown\n")
}

注意:num.(type)必须要在num声明为接口时才能使用。

panic/recover

panic相当于java的throw,不过在Go中一般称作运行时恐慌,而不是异常。panic一旦引发,就会通过调用方法传播直到程序崩溃,Go中提供了类似于catch的recover来拦截恐慌。recover被调用后,将会一个interface类型的恐慌结构,否则返回nil。

recover一般是结合defer使用的,defer语句将在下一部分阐述:

func panicTest(val int)  {
	if val < 0 {
		 panic("错误的值")
	}
	fmt.Printf("value is %d\n", val)
}

func recoverTest(val int)  {
	defer func() {
		if e := recover(); e != nil {
			fmt.Printf("拦截异常成功!\n")
		}
	}()
	panicTest(val)
}


func main() {
	recoverTest(10)
	recoverTest(-10)
	recoverTest(10)
}

在上面的代码中,首先向recoverTest传入一个合法的值,函数正常执行。然后传入一个非法值,在panicTest时抛出错误,但是在recoverTest函数中在defer进行了处理,将抛出的错误进行了处理,所以程序没有崩溃,第三次正常执行。

defer

在测试异常机制时,使用了defer语句,该语句用于延迟调用指定的函数,将一个方法延迟到包裹该方法的方法返回时执行,该函数被称之为延迟函数。它的运行机制如下:

  • 在外部函数执行完毕时,当延迟函数全部执行完毕时,外部函数才是真正的执行完毕。
  • 当外部函数代码中因此panic时,当延迟函数全部执行完毕,该panic才会扩散至调用函数。

可以通过defer定义多个延迟函数,此时会将所有的延迟函数按照代码顺序进行压栈处理,当defer被触发时,所有的压栈方法都会出栈。

注意,只有在方法返回时,defer才会被调用,如果直接使用类似os.Exit(0)退出的话,defer不会被触发。

defer的闭包使用

先来看下面这个代码:

var arr [5]struct{}
for i := range arr {
    defer func() { fmt.Println(i) }()
}

for i := range arr {
    defer func(i int) { fmt.Println(i) }(i)
}

运行之后结果为432104444,这是为什么呢?

首先43210是第二个for循环的输出,44444是第一个for循环的输出,如前面讲的,延迟函数是通过栈存储的,遵循FILO原则。在第一个for循环中,变量i在defer被声明时就已经确定值了,即实际上它等价于:defer func() { fmt.Println(4) }(),i等于4的原因是因为defer是在函数执行完成时开始的,此时for循环已经过了5次,i结果累加也就变成了4。而在第二个for循环中,因为defer语句接收i作为函数参数,所以每次存储的i的值都是不一样的。

并发

在Go中,处理并发的是更轻量级的线程——协程,虽然协程的出现比线程还要早,但是由于当时协程由于是非抢占式的,需要用户手动释放运行权(相当于单线程),所以并没有受到重视。但是Go在此基础上自己实现了协程的调度机制,使用户不需要手动设置。

每一个并发执行的活动被称为goroutine,当一个程序启动的时候,只有一个goroutine来调用main函数,称它为主goroutine,新的goroutine通过go语句进行创建。

goroutine

goroutine的简单使用如下:

func main() {
	go func() {
		fmt.Printf("Hello World!\n")
	}()
	time.Sleep(1 * time.Second) // 让主线程睡眠1s
	fmt.Printf("exit\n")
}

相对于java需要继承或实现相关的类,Go对于并发的使用就简单很多 ,只需要使用go语句就能在主goroutine上开启子goroutine。

sync

Go的sync包提供了基本的同步方法,比如互斥锁、信号量等。但是Go并不推荐这个包中大多数的方法,因为Go提倡使用以共享内存的方式来通信。Go用于控制并发的方法主要是下面的channel(通道)。

sync中主要有:

  • WaitGroup:用来等待一组goroutines的结束。

    func helloWorld() {
       fmt.Printf("Hello World\n")
    }
    
    func main() {
       var wg sync.WaitGroup // 声明group
       wg.Add(10) // 指定group个数
       for i := 0; i < 10; i++ {
          go func() {
             helloWorld()
             wg.Done() // group次数减1
          }()
       }
       wg.Wait() // 等待直到group次数为0
    }
    
  • Map:就是在并发环境下使用的Map,跟java的CurrentHashMap作用差不多。

    func main() {
        var scene sync.Map
        scene.Store("test1", 97) // 将键值对保存到sync.Map
        scene.Store("test2", 100)
        scene.Store("test3", 200)
        fmt.Println(scene.Load("test1")) // 从sync.Map中根据键取值
        scene.Delete("test1") // 根据键删除对应的键值对
        // 遍历所有sync.Map中的键值对,遍历需要提供一个匿名函数,将结果返回
        scene.Range(func(k, v interface{}) bool {
            fmt.Printf("key:%s ----> value:%s\n", k, v)
            return true
        })
    }
    
  • Mutex:互斥锁使用:

    func helloWorld() {
       fmt.Printf("Hello World\n")
    }
    
    func main() {
       var lock sync.Mutex // 声明互斥锁
       for i := 0; i < 10; i++ {
          lock.Lock() // 加锁
          go func() {
             helloWorld()
             lock.Unlock() // 解锁
          }()
       }
    }
    
  • RWMutex:跟java的读写锁差不多,可以分为读锁和写锁进行操作。

  • Once:指定某个方法只执行一次:

    func helloWorld() {
       fmt.Printf("Hello World\n")
    }
    
    func main() {
       var once sync.Once
       done := make(chan bool)
       for i := 0; i < 10; i++ {
          go func() {
             once.Do(helloWorld) // 调用指定方法
             done <- true
          }()
       }
       for i := 0; i < 10; i++ {
          <-done
       }
    }
    
  • Pool:缓存对象池,Go的Pool在很多地方都用到了,比如http连接的创建、数据库连接的创建等。

    func helloWorld() interface{} {
       fmt.Printf("Hello World\n")
       return 0
    }
    
    func main() {
       p := &sync.Pool{ // Pool的缓存对象数量没有限制
          New:helloWorld,
       }
       task1 := p.Get().(int) // 获取返回值
       p.Put(10) // 修改值
       runtime.GC() // GC时清除所有的pool里面的所有缓存的对象
       task2 := p.Get().(int)
       fmt.Printf("a = %d, b = %b\n", task1, task2)
    }
    

channel

Go语言为不同线程间的通信提供了channel支持,通道是可以让一个goroutine发送特定的值到另外一个goroutine的通信机制。

示例如下所示:

func sender(ch chan string) {
	ch <- "test1" // 从channel中写数据
	ch <- "test2"
	ch <- "test3"
	ch <- "test4"
}

func recver(ch chan string) {
	for {
		fmt.Println(<-ch) // 从channel中读数据
	}
}

func main() {
	ch := make(chan string) // 创建一个channel
	go sender(ch)
	go recver(ch)
	time.Sleep(1e9)
}

上面代码中创建的channel是无缓冲的,也可以使用make(chan string, 2)创建有缓冲的channel。

我们可以在函数的参数列表指定是只读channel、只写channel、可读可写channel:

// 只读channel
func test1(out <-chan int)

// 只写channel
func test2(in chan<- int)

// 可读可写channel
func test3(ch chan int)

channel本质上传递的是数据的拷贝。

select可用于监听多个channel的数据流入

select {
    case v, ok := <-chann:
        if ok {
            fmt.Println(v)
        } else {
            fmt.Println("close")
            return
        }
    default:
    	fmt.Println("waiting")
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值