博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Go语言立地入门
阅读量:3933 次
发布时间:2019-05-23

本文共 11897 字,大约阅读时间需要 39 分钟。

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 inta = 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中有很多预定义类型,可以将它们简单地划分为基本类型和高级类型,基本类型有:

类型 宽度 初始值
bool 1 false
byte 1 0
rune 4 0
int/uint - 0
int8/uint8 1 0
int16/uint16 2 0
int32/uint32 4 0
int64/uint64 8 0
float32 4 0.0
float64 8 0.0
complex64 8 0.0 + 0.01
complex128 16 0.0 + 0.01
string - “”

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

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

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

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

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

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

var str string = strconv.Itoa(num) // 数字-->字符串 Itoa是Formatnum2,_  := 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}// 如果返回列表中指定了返回值的字面量,那么可以直接returnfunc 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:

// 只读channelfunc test1(out <-chan int)// 只写channelfunc test2(in chan<- int)// 可读可写channelfunc 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")}

转载地址:http://gcqgn.baihongyu.com/

你可能感兴趣的文章
BMP 文件格式的详解
查看>>
9针串口引脚定义
查看>>
QT4 QWebView的使用
查看>>
QT QWebView/QWebEngineView使用
查看>>
ARM Linux Kernel 编译结果 Image zImage uImage映像的区别
查看>>
SVN 删除用户名和密码
查看>>
EXPORT_SYMBOL() 错误--warning: type defaults to 'int' in declaration of 'EXPORT_SYMBOL'
查看>>
Qt 使用 QSettings 读写ini文件
查看>>
Uboot LCD 添加进度条功能
查看>>
Git diff 使用 vimdiff 对比差异
查看>>
使用debugfs来调试内核
查看>>
Qt4 程序 QWS 启动参数详解
查看>>
QT 支持鼠标和触摸屏输入
查看>>
svn diff 使用 vimdiff 对比差异
查看>>
使用 vimdiff 比较文件的技巧
查看>>
Ubuntu 安装 SSH 服务
查看>>
Git commit 设置提交日志编辑器
查看>>
Git config 配置文件详解
查看>>
Git config alias 设置命令别名
查看>>
Git 追踪内容详解
查看>>