欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

GO语言快速入门

程序员文章站 2022-06-06 21:26:25
...

简介

特性

强类型、编译型、并发型,并具有垃圾回收功能的编程语言。

运行方式

直接运行:
go run helloworld.go

编译后再运行:

$ go build helloworld.go

$ ./helloworld
Hello,world

语法

  • “{”必须和func同一行
  • 首字母大写的是可以由package外部访问的;首字母小写的只能在package内部访问

数据类型

类型 名称 长度 零值 说明
bool 布尔类型 1 false 其值不为真即为假,不可以用数字代表true或false
byte 字节型 1 0 uint8别名
rune 字符类型 4 0 专用于存储unicode编码,等价于uint32
int, uint 整型 4或8 0 32位或64位
int8, uint8 整型 1 0 -128 ~ 127, 0 ~ 255
int16, uint16 整型 2 0 -32768 ~ 32767, 0 ~ 65535
int32, uint32 整型 4 0 -21亿 ~ 21 亿, 0 ~ 42 亿
int64, uint64 整型 8 0
float32 浮点型 4 0 小数位精确到7位
float64 浮点型 8 0 小数位精确到15位
complex64 复数类型 8
complex128 复数类型 16
uintptr 整型 4或8 ⾜以存储指针的uint32或uint64整数
string 字符串 “” utf-8字符串

变量声明

第一种,指定变量类型,声明后若不赋值,使用默认值。

var v_name v_type
v_name = value

第二种,根据值自行判定变量类型。

var v_name = value

第三种,省略var, 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误。

v_name := value

// 例如
var a int = 10
var b = 10
c := 10

多变量声明

var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3

a, b, c := 5, 7, "abc"

如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误

如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。

常量

const identifier [type] = value

iota

iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。

const (
    a = iota // 0
    b			// 1
    c			// 2
)
const (
    i=1<<iota // 1
    j=3<<iota // 3 * 2 =6
    k			 // 3 * 4 = 12
    l			 // 3 * 8 = 24
)

运算符

  • 只有后自增和后自减 a++

流程控制

if

// if 后不用括号
if a < 20 {
   fmt.Printf("a 小于 20\n" )
}

switch

// case后不用break。自动break 如果要继续下一个case则需要fallthrough
switch marks {
  case 90: grade = "A"
  case 80: grade = "B"
  case 50,60,70 : grade = "C"
  default: grade = "D"  
}

switch {
  case grade == "A" :
     fmt.Printf("优秀!\n" )     
  case grade == "B", grade == "C" :
     fmt.Printf("良好\n" )      
  case grade == "D" :
     fmt.Printf("及格\n" )      
  case grade == "F":
     fmt.Printf("不及格\n" )
  default:
     fmt.Printf("差\n" );
}

//支持多条件匹配
switch a {
    case 1,2,3,4:
    default:
}

循环

// 类似c的for
for init; condition; post { }
// 类似c的while
for condition { }
// range 对slice map array
for key, value := range oldMap {
    newMap[key] = value
}

函数

func function_name( [parameter list] ) [return_types] {
   函数体
}

函数可以作为值来传递

func (name MyStruct) imp() string{
    print("这是实现方法的写法")
}

func sum(x int,y int) int{
    print("这是正常写法")
}

传递方式

和其他语言不同的是,go语言在将数组名作为函数参数的时候,参数传递即是对数组的复制。在形参中对数组元素的修改都不会影响到数组元素原来的值。

在使用slice作为函数参数时,本质也是传值,但由于slice由指针、长度、容量组成,因此也可以看成传递一个地址拷贝,即将底层数组的内存地址复制给参数slice。这时,对slice元素的操作就是对底层数组元素的操作,但slice本身不改变,若在函数内append,底层数组改变,但传入的参数的外部的slice长度不变。

切片传递的时候,等效于从原始切片中再切了一次。原始切片slice和参数s切片的底层数组是一样的。因此修改函数内的切片,也就修改了数组。如果在函数内,append操作超过了原始切片的容量,将会有一个新建底层数组的过程,那么此时再修改函数返回切片,应该不会再影响原始切片。

slice或者array作为函数参数传递的时候,本质是传值而不是传引用。传值的过程复制一个新的切片,这个切片也指向原始变量的底层数组。(个人感觉称之为传切片可能比传值的表述更准确)。函数中无论是直接修改切片,还是append创建新的切片,都是基于共享切片底层数组的情况作为基础。也就是最外面的原始切片是否改变,取决于函数内的操作和切片本身容量。

因此要传引用的话还是需要用指针

func main() {
    slice := make([]int, 2, 2)
    for i := 0; i < len(slice); i++ {
        slice[i] = i
    }

    fmt.Printf("slice %v %p \n", slice, &slice)

    ret := changeSlice(slice)
    fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret)

    ret[1] = -1111

    fmt.Printf("slice %v %p, ret %v \n", slice, &slice, ret)
}

func changeSlice(s []int) []int {
    fmt.Printf("func s %v %p \n", s, &s)
    s[0] = -1
    s = append(s, 3)
    s[1] =  1111
    return s
}

//输出
slice [0 1] 0xc42000a1a0 
func s [0 1] 0xc42000a200 
slice [-1 1] 0xc42000a1a0, ret [-1 1111 3] 
slice [-1 1] 0xc42000a1a0, ret [-1 -1111 3]

append之前操纵的是同一个数组

匿名函数

Go 语言支持匿名函数,可作为闭包。匿名函数是一个"内联"语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

数组

数组是值类型

在 Go 中数组是值类型而不是引用类型。这意味着当数组变量被赋值时,将会获得原数组(译者注:也就是等号右面的数组)的拷贝。新数组中元素的改变不会影响原数组中元素的值。

var variable_name [SIZE] variable_type
var balance [10] float32


// 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
// 如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 多维数组
var threedim [5][10][4]int

指针

// 当一个指针被定义后没有分配到任何变量时,它的值为 nil。
var ip *int  

结构体

type Books struct {
   title string
   author string
   subject string
   book_id int
}

func main() {

    // 创建一个新的结构体
    fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})

    // 也可以使用 key => value 格式
    fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})

    // 忽略的字段为 0 或 空
   fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"})
}

使用结构体指针访问结构体成员,也使用 “.” 操作符

Slice

切片本身不包含任何数据。它仅仅是底层数组的一个上层表示。对切片进行的任何修改都将反映在底层数组中。

当若干个切片共享同一个底层数组时,对每一个切片的修改都会反映在底层数组中。

// 可以声明一个未指定大小的数组来定义切片
var identifier []type
// 或使用make()函数来创建切片 capacity是可选参数
make([]T, length, capacity)

初始化

s :=[] int {1,2,3 } // cap=len=3 创建了一个长度为 3 的 int 数组,并返回一个切片给 c。
s := arr[startIndex:endIndex]  // s是arr的引用 缺省startIndex时将表示从arr的第一个元素开始,缺省endIndex时将表示一直到arr的最后一个元素

len() cap()

func main() {
   var numbers = make([]int,3,5)

   printSlice(numbers)
}

func printSlice(x []int){
   fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)
}

append() 和 copy()

numbers = append(numbers, 2,3,4)
/* 拷贝 numbers 的内容到 numbers1 */
copy(numbers1,numbers)

当新元素通过调用 append 函数追加到切片末尾时,如果超出了容量,append 内部会创建一个新的数组(没超的话直接改原来的)。并将原有数组的元素被拷贝给这个新的数组,最后返回建立在这个新数组上的切片。这个新切片的容量是旧切片的二倍(译者注:当超出切片的容量时,append 将会在其内部创建新的数组,该数组的大小是原切片容量的 2 倍。最后 append 返回这个数组的全切片,即从 0 到 length - 1 的切片)

切片保留对底层数组的引用。只要切片存在于内存中,数组就不能被垃圾回收。这在内存管理方便可能是值得关注的。假设我们有一个非常大的数组,而我们只需要处理它的一小部分,为此我们创建这个数组的一个切片,并处理这个切片。这里要注意的事情是,数组仍然存在于内存中,因为切片正在引用它。

解决该问题的一个方法是使用 copy 函数 func copy(dst, src []T) int 来创建该切片的一个拷贝。这样我们就可以使用这个新的切片,原来的数组可以被垃圾回收。

range

用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素

nums := []int {2, 3, 4}
for i, num := range nums {
    if num == 3 {
        fmt.Println("index:", i)
    }
}
kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
}
for i, c := range "go" {
    fmt.Println(i, c)
}

map

var countryCapitalMap map[string]string /*创建集合 */
countryCapitalMap = make(map[string]string)
captial, ok := countryCapitalMap [ "美国" ]
delete(countryCapitalMap, "France") // 参数为map和key

类型转换

var sum int = 17
var count int = 5
var mean float32
   
mean = float32(sum)/float32(count)
fmt.Printf("mean 的值为: %f\n",mean) // 3.400000

接口

  • 接⼝命名习惯以 er 结尾
  • 接口只有方法声明,没有实现,没有数据字段
  • 如果一个类型实现了一个接口要求的所有方法,那么该类型实现了这个接口,无须声明实现哪个接口。
  • 接口的赋值规则:仅当一个表达式实现了一个接口时,这个表达式才可以赋值给该接口
  • 空接口(interface{})不包含任何的方法,正因为如此,所有的类型都实现了空接口
type Phone interface {
    call()
}

type NokiaPhone struct {
}

// 方法
func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type IPhone struct {
}

func (iPhone IPhone) call() {
    fmt.Println("I am iPhone, I can call you!")
}

func main() {
    var phone Phone

    phone = new(NokiaPhone)
    phone.call()

    phone = new(IPhone)
    phone.call()

}

类型断言

把对象赋给接口之后,若要得到它原来的类型,可以用类型断言。类型断言是一个作用于接口对象上的操作,写出来类似 x.(T),其中x是接口对象,而T是一个类型。

var v1 interface{} = 1     
value, ok := v1.(int) // 1 true

错误处理

使用errors.New 可返回一个错误信息

func Sqrt(f float64) (float64, error) {
    if f < 0 {
        return 0, errors.New("math: square root of negative number")
    }
    // 实现
}

result, err:= Sqrt(-1)

if err != nil {
   fmt.Println(err)
}

并发

原理

Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可。goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。

func say(s string) {
    for i := 0; i < 5; i++ {
            time.Sleep(100 * time.Millisecond)
            fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

等待

sync.waitGroup :等待所有goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成

GOMAXPROCS

调用 runtime.GOMAXPROCS() 用来设置可以并行计算的CPU核数的最大值,并返回之前的值。

通道(channel)

是用来传递数据的一个数据结构。从设计上确保,在同一时刻只有一个 goroutine 能从中接收或放入数据。发送和接收都是原子操作,不会中断。

通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。

用途:

  • 使用通道来同步goroutine
  • 使用异步管道来保护临界资源(令牌 谁拿到了才能用,使用完退回管道)
  • 用管道来发送事件
同步通道

缓冲长度为0的channel称为同步管道,可以用来同步两个routine

  • 发送操作被阻塞,直到接收端准备好接收
  • 接收操作被阻塞,直到发送端准备好发送
异步通道

缓冲长度大于0的channel称为异步管道。
异步 channel,就是给 channel 设定个 buffer 值

  • 在 buffer 未填满的情况下 ,不阻塞发送操作。
  • 在buffer 未读完前,不阻塞接收操作。
// 声明通道
ch := make(chan int)

var ch1 chan int       // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int     // ch3是单向channel,只用于读取int数据


// 通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小

ch := make(chan int, 2)

// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
// 而不用立刻需要去同步读取数据
ch <- 1
ch <- 2

// 获取这两个数据
fmt.Println(<-ch)
fmt.Println(<-ch)

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)
    // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
    // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
    // 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
    // 会结束,从而在接收第 11 个数据的时候就阻塞了。
    for i := range c {
            fmt.Println(i)
    }
}

select

每个case语句里必须是一个IO操作

 select {    
	case <-chan1:        
		// 如果chan1成功读到数据,则进行该case处理语句    
	case chan2 <- 1:        
		// 如果成功向chan2写入数据,则进行该case处理语句    
	default:        
		// 如果上面都没有成功,则进入default处理流程    
	}

defer

defer后面的函数在defer语句所在的函数执行结束的时候会被调用;

defer语句:一个普通的函数或方法调用,在调用前加上关键字 defer
函数和参数表达式会在语句执行时求值,但是无论在正确情况(如执行return语句等)还是非正常情况(如宕机),实际的调用推迟到包含defer语句的函数结束后才执行。
defer语句经常使用于成对的操作,比如打开和关闭、连接和断开、加锁和解锁,即使是再复杂的控制流、资源在任何情况下都能正确释放
如果一个函数中有多个defer语句,它们会以LIFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

标准库

文档

  • 输入输出。这个分类包括二进制以及文本格式在屏幕、键盘、文件以及其他设备上的输
    入输出等,比如二进制文件的读写。对应于此分类的包有bufio、 fmt、 io、 log和flag
    等,其中 flag 用于处理命令行参数。
  • 文本处理。这个分类包括字符串和文本内容的处理,比如字符编码转换等。对应于此分
    类的包有encoding、 bytes、 strings、 strconv、 text、 mime、 unicode、 regexp、
    index和path等。其中path用于处理路径字符串。
  • 网络。这个分类包括开发网络程序所需要的包,比如Socket编程和网站开发等。对应于此
    分类的包有: net、 http和expvar等。
  • 系统。这个分类包含对系统功能的封装,比如对操作系统的交互以及原子性操作等。对
    应于此分类的包有os、 syscall、 sync、 time和unsafe等。
  • 数据结构与算法。对应于此分类的包有math、 sort、 container、 crypto、 hash、
    archive、 compress和image等。因为image包里提供的图像编解码都是算法,所以也
    归入此类。
  • 运行时。对应于此分类的包有: runtime、 reflect和go等。

面向对象

方法

func后面加一个参数表示方法
Go可以将方法绑定到任何类型上,可以很方便的为简单的类型(如 int等)定义附加的方法
与普通函数声明类似,只是在函数名字前面多了一个参数,该参数把方法绑定到这个参数对应的类型上

type Point struct { X, Y float64}
func Distance (p, q Point) float64 { /* 普通函数 */
	return math.Hypot(q.X-p.X,q.Y-p.Y)
}
func (p Point) Distance (q Point) float64 { /*Point 类型的方法*/
	return math.Hypot(q.X-p.X,q.Y-p.Y)
}

p称为方法的接收者(可以是类型的值或是指针),接收者不使用特殊名(如this),而可以选择接收者名字

用指针接收者

func (p *Point) ScaleBy (factor float64) {
	p.X *= factor
	p.Y *= factor
}

习惯上遵循如果Point的任何一个方法使用指针接收者,那么所有的Point方法都应该使用指针接收者,用指针的调用方法:

p := Point{1, 2}
(&p).ScaleBy(2)
p.ScaleBy(2)  //编译器会对变量进行 &p 的隐式转换,只有变量才允许这么做
// 如下面调用是错误的 
Point{1, 2}.ScaleBy(2)

不允许本身是指针的类型进行方法声明
没有指向指针的用法

方法变量

一个函数,把方法绑定到一个接收者上,函数只需要提供实参而不需要提供接收者就能调用

p := Point{1, 2}
q := Point{2, 4}
distanceFromP := p.Distance //distanceFromoP 就叫做方法变量
distanceFromP(q)

方法表达式

T.f 或者(*T).f,其中T是类型
是一种函数变量
把原来方法的接收者替换为函数的第一个形参
distance := Point.Distance //方法表达式
distance(p, q)

只支持封装,不支持继承和多态,go中用面向接口来做继承和多态的任务
go中没有class ,只有struct

不支持重载
构造函数用工厂方法

反射

在编译时不知道类型的情况下,更新变量、在运行时查看值、调用方法以及直接对它们的布局进行操作的机制,叫做反射

反射功能由reflect包提供,定义了两个重要的类型

  • Type

    • 有很多方法的接口,这些方法可以用来识别类型以及透视类型的组成部分。
    • 接口只有一个实现,即类型描述符,接口值中的动态类型也是类型描述符。
    • reflect.TypeOf函数接收任何的interface{}参数,并把接口中的动态类型以reflect.Type形式返回。
    t := reflect.TypeOf(3)
    fmt.Println(t)  // “int”
    
  • Value

    包含一个任意类型的值
    ValueOf函数接受任意的interface{}参数,并把接口的动态值以reflect.Value的形式返回。

stu := student{Name:"zhangsan", Age:25} 
t := reflect.TypeOf(stu) //获取对象的类型名称fmt.Println("class name:", t.Name()) 

//获取stu对象的成员名称和值 
v := reflect.ValueOf(stu)
for i := 0; i < t.NumField();i++{ 
    field := t.Field(i)
    fmt.Println(field.Name, "type:", field.Type) 
    fmt.Println(field.Name, "value:", v.Field(i)) 
} 
//获取stu的方法
for i := 0; i < t.NumMethod(); i++ {
    f := t.Method(i) 
    fmt.Println(f.Name, f.Type) 
} 

//设置stu的age值为100
v := reflect.ValueOf(&stu)
v = v.Elem()
field := v.FieldByName("Age") 
field.SetUint(100) 

//调用stu的setName方法
v = reflect.ValueOf(&stu)
m := v.MethodByName("SetName")
args := []reflect.Value{reflect.ValueOf("liSi")}
m.Call(args)
fmt.Println(stu) 

其他

sync.WaitGroup

简单使用就是在创建一个任务的时候wg.Add(1), 任务完成的时候使用wg.Done()来将任务减一。使用wg.Wait()来阻塞等待所有任务完成。

注意点:传递给函数时要用地址传

相关标签: go 入门教程