由于有过C++和Java的学习经验,所以Go语言的学习并没有想象的那么困难,在这里记录新语言的学习过程,希望对同样情况的同学有帮助;
由于是从Java转过来学习GO,所以会以两种语言对比进行学习;
注意:打印的时候通过%d、%T可以控制打印对应变量的值
比如fmt.printf("变量类型为: %T", myarr)
结果就是对应的数据类型
初识Go语言
首先,Go是一门十分新的语言,由于它基于C语言开发,所以在语法中可以看出很多C语言的风格特点,但是又融合了其他语言的特点,比如在变量定义上就有类似Python的设计。
给人的感觉就像网络上常见的一个梗:
坏消息,是缝合怪;好消息,都缝了
话不多说,和所有编程语言的共同起点Hello World来进入Go语言的学习之旅;
package main
import "fmt"
func main() {
fmt.println("Hello World")
}
首先要注意的是,Go语言中的fmt是一个标准库,在这个库中处理输入和输出,比较特别的是,package main生命的使main.go所在的包,也就是函数的入口必须是main,这点和Java还是有所不同的;
PS:代码结束不需要写;对于写惯了Java的朋友可能需要适应一段时间
变量类型
变量的声明
与Java中不同的是,Go语言的变量类型写在变量名后后面,直接声明变量可以采用关键字var;
var a int = 1
可以直接赋值,如果没有对于基本数据类型来说是有默认值的;
var a = 1
Go语言中可以根据赋值的数据自动分配变量类型,通过复制自动被确认为int类型
a := 1
也可以使用这种更加简化的写法,直接通过:=赋值并初始化
变量类型
Go语言中的变量类型有如下几种
空值:nil
整型类型: int, int8, int16, int32, int64, uint8, uint16, …
浮点数类型:float32, float64
字节类型:byte (等价于uint8)
字符串类型:string
布尔值类型:boolean,(true 或 false)
Go语言中对变量类型的区分是十分细致的,比如int就有多种形式的int,如果直接用int作为变量类型,会根据操作系统的位次决定;其中uint表示无符号的int类型;
顺便一提,string小写真的让我适应了好久,经常写成String;
其中uint表示无符号的int类型;
字符串
Go语言中使用UTF8编码,也就是对于英文字符来说每个字符仅需要占1byte
package main
import "fmt"
func main() {
string s := "talk2zbw学习Go语言"
fmt.println(s)
fmt.println(len(s))
fmt.println(s[1],string(s[1]))
}
go中string可以直接通过索引获取对应位置的字符,并且通过len可以检查字符串的长度,这个长度表示的是所占字节数(中文占3个字节,英文字符占1个字节),因为go中字符串也使用byte数组实现(JDK8之后中的Java类似)
如果我们想要检查字符串中字符的个数,处理方法是将string转化为rune数组
string s := "talk2zbw学习Go语言"
runeArray := []rune(s)
fmt.println(len(runArray)
这样显示的就是14,也就是字符个数了
如果我们想知道某一个变量的类型,可以使用反射机制
reflect.TypeOf(runArray[1]).kind()
这个结果是int32,可以看到转换成\[]rune类型之后,字符串中每个字符无论占用几个字节都用int32表示,所以也能正确的处理中文字符
数组和切片
数组arry
我们可以按照上面所说的变量声明方法通过var初或者声明时初始化的方式声明数组
var arr [4]int
var arr2 = [4]int{1,2,3,4}
这里要注意这个[]符号是写在变量类型前面的
同样我们可以直接通过索引的方式来访问数组中的变量
arr2[1] += 3
fmt.println(arr2)
// 1,5,3,4
数组在声明之后不可以更改,如果想要对数组进行操作需要使用切片
切片slice
首先我们需要声明切片
slice1 := make([]float32,3,5)
// 声明一个容量为5长度为3的切片
fmt.pritnlb(len(slice1))
这里表示我们声明了一个长度为3容量为5的切片
切片包含三个组件:容量、长度、和一个指向底层实现数据结构数组的指针
这里虽然开辟了容量为5,但是长度3标记了切片的合法访问范围
这个时候不可以直接通过索引访问第四个位置,只能通过append添加
添加后len长度更新为4,此时就能访问第四个索引了
与数组不同,切片可以随时的扩展
与Java中的ArrayList很像,slice同样可以预分配空间并在使用过程中根据实际情况动态的拓展
这里长度为3,由于我们没有初始化值默认用0填补
也就是当前切片为[0 0 0]
slice1 = append(slice1,1,2,3,4)
// slice1在append之后变为[0,0,0,1,2,3,4]
超出容量自动扩容(原来的2倍)
如果我们想获取slice中的部分,可以按照如下进行操作
slice2 := slice1[:3] // [0,0,0]
slice3 := slice1[3:] // [1,2,3,4]
slice4 := slice1[1:3] //[0,0]
这里如果写过python会很熟悉这种表达方式
两个参数分别对应起始和结束索引左闭右开
但是要注意这里slice2等slice1的部分实际上指向的还是原切片,只不过是部分
比如我们通过slice2[0] = 1 slice1中的0索引位置也会改变
因为使用的是同一个底层数组
如果想实现深拷贝可以使用copy方法
slice5 := make([]float32,7)
copy(slice5,slice1) //将slice1中值拷贝到slice5中
总结几种切片的声明方式
slice1 := []int{1,2,3,4}
var slice2 []int // 声明但是没有给slice分配空间
slice3 := make([]int,3) 开辟三个空间默认值为0
var slice4 []int = make([]int,3)
判断slice是否为空一般用nil来进行判断
很多人学习到这里的时候容易把切片和数组搞混,因为切片本身就是数组的抽象,相当于一个动态数组,所以在声明的时候如果在[]中制定了容量就是固定长度的数组,反之没有指定[]的就是切片,不过实际开发中数组使用的场景相对较少,大多数情况以切片进行函数之间的传递,尤其是作为形参,如果我们定义一个参数为长度为4的数组,那么是不能传递长度为其它值的数组的灵活性较差。只有部分固定长度参数场景会用到
并且切片作为参数传递的时候是引用传递,因为切片本身就是保存了一个指向底层数组的指针,并且不同长度的动态数组它们的形参一致
map
和Java中的HashMap类似的数据结构,存储的对象是键值对
使用方式上也和Java中类似
map1 := make(map[string]int,10) // 声明一个string-int的键值对
// 也可以预先分配空间
map2 := map[string]int{
"zbw":18,
"8v":24
}
// 不在声明的时候初始化也可以
var map3 map[string]string // 只声明不会初始化采用默认值nil
// 声明并初始化
map2[zbw] = 20 // 直接通过键给值赋值
同样会动态的扩容,扩容为原来的二倍
可以直接通过println进行打印
因为底层是哈希存储所以不保证插入顺序和实际顺序一致
map同样是引用传递
map的基本使用
添加key-value
// 直接通过key进行添加
map1["Java"] = 1
map1["Golang"] = 2
删除操作
delete(map1,"Java")
修改
map1["Golang"] = 1
遍历
for key,value := range map1{
fmt.println(key,value)
}
指针pointer
Go语言中是有指针这个概念的,表示某个对象或者值的地址,与C++中一样通过符号\*定义,通过&取址符获取地址。
int a := 1
var p *int = &a
*p = 2 // 改变了a的值为2
这里*p表示访问指针所指向的值,也就是解引用
fmt.println(p) // 打印地址值,比如 0xc0000140a8
fmt.println(*p) // 打印指向值 也就是2
Go语言中参数是按值传递的,如果跟不适用指针那么就是传入一个参数的副本,但是如果对参数使用指针就能通过参数的传递在方法内改变外部变量
简单来说,就是指针变量保存的是地址,通过*指针的当时解引用,表示指针指向的值
这种方式就可以修改对应地址的变量了
常量const与itoa
const length int = 10
设置一个常量,只读属性,不可更改
可以用来定义枚举类型,比如
const (
BEIJING = 0
SHANGHAI = 1
SHENZHEN = 2
)
但是对于这种顺序增加的值一个一个去定义是很麻烦的,所以可以使用itoa
const (
BEIJING = iota
SHANGHAI
SHENZHEN
)
这样第一行iota = 0,在第二行开始为1顺序增加
这样就不需要自行定义修改了
也就是我们第一个变量定义为iota了下面的均自动添加iota,并且iota递增
同理我们定义BEIJING为10 \* iota,第二个也是10 \* iota
这样值就分别为0 10 20.....
const (
a,b = iota+1,iota+2
c,d
e,f
g,h = iota 2,iota 3
i,k
)
这样也是一样的,第一行iota=1所以结果ab是1和2
然后cdef就分别是2、3、3、4
到了gh这一行iota变为3对应的就是6和9以此类推
条件语句
if else
基本和其它语言中的用法一致
区别是变量的声明可以写在条件中,和条件用逗号分隔
if int a := 4,a > 2{
fmt.println("a大于2")
}else{
fmt.println("a不大于2")
}
switch
Go语言中的switch不需要break,如果匹配到某一个case中,执行完成后默认不会继续向下执行
int age := 18
switch age{
case 18:
fmt.println("年龄为:18")
// fallthrough
case 19:
fmt.println("年龄为:19")
default:
fmt.println("年龄未知")
}
如果想要让匹配到case中执行完毕继续执行
需要用fallthrough
在实际开发中switch的使用频率并不多
for循环
Go语言中没有while循环,因为设计者认为实际开发中while循环用的并不多,并且while循环可以转变为for循环
sum := 0
for i := 0; i < 10; i++ {
if sum > 50 {
break
}
sum += i
}
和其他语言用法相同,同样可以使用continue和break
对于数组、切片、字典可以使用for range进行遍历
这种用法类似于增强for
map1 := map[string]int{
"zbw":18,
"8v":24
}
for key,value := range m1{
fmt.println(key,value)
}
// zbw 18
// 8v 24
对于切片和数组for range返回的两个值分别是索引和索引对应的值
函数
函数多返回值
这一点是go中区别最大的地方,go语言中允许一个函数有多个返回值
func method (a string,b int) int{
fmt.println("a=",a)
fmt.println("b=",b)
c := 100
return c
}
func main(){
method("abc",1)
}
// a=abc
// b=1
func method2 (a string,b int) (int,int){
fmt.println("a=",a)
fmt.println("b=",b)
return 2,3
}
go语言中的返回值可以有多个,我们也可以给返回值起名,上面是通过匿名的方式返回
func method3 (a string,b int) (d int,e int){
fmt.println("a=",a)
fmt.println("b=",b)
d = 3
e = 4
return d,e
}
这种有名称的返回值,如果我们不给d和e赋值
d和e是作为两个局部变量进行赋值的
默认初始化值为0(防止野指针出现,go语言初始化默认的值都是0)
init函数
一般情况下通过main作为函数的主入口,然后通过import导入的包逐层去深入
直到一个没有导入其他包代码中,最后逐层返回
所以可以看到init函数一般是优先于main函数的
如果我们在主程序中需要在main前实现一些初始化可以用init
同理在包代码中需要初始化一些内容也可以使用init、
比如我们创建两个路径分别对应包lib1和lib2
并定义init方法打印一个lib1或者lib2
然后再main文件中import这两个包
这样即使main函数中没有进行处理由于导入这两个包也会执行对应包的文件中的init方法
对外的API开头要大写,小写表示私有外部不能访问
import匿名及别名导包
如果我们导入一个包,但是go中不使用这个包就会报错
所以我们可以使用_匿名导入,这样即使不适用也不会报错
当我们导入的包比较长可以起一个别名比如mylib2
这样下面通过mylib2使用对应包方法(替代lib2这个名字,因为有的包可能名字很长)
如果. xxx/xxx/xxx
表示直接把这个包导入当前包,这样就不需要使用包名.的方式调用了
直接可以调用包种方法,但是不建议这样做
因为可能导致包名冲突,比如多个包中有相同名字的函数或者子包
defer关键字
func main(){
defer fmt.prinln("函数运行结束")
fmt.prinln("函数运行开始")
fmt.prinln("函数运行中")
}
结果就是
函数运行开始
函数运行中
函数运行结束
使用defer修饰的代码会在当前方法执行结束之前运行,类似于Java中的finally
当然我们也可以使用多个derfer
func main(){
defer fmt.prinln("函数运行结束")
defer fmt.prinln("函数运行结束前")
fmt.prinln("函数运行开始")
fmt.prinln("函数运行中")
}
结果就是
函数运行开始
函数运行中
函数运行结束前
函数运行结束
这是因为go语言中的defer采用压栈的方式,第一个defer先入栈然后第二个入栈
出栈顺序自然就是运行结束前这个打印先执行
也就是只有程序逻辑方法生命周期结束前执行
Struct
首先可以通过type给已有的类其别名
type myint int
也可以通过struct定义一个结构体
type Book struct{
title string
auth string
}
在外部可以声明这个struct的实例化对象
struct的方法
type Person struct{
Name string;
Age int;
}
func (this Person)GetName(){
fmt.println{"Name = " this.Name}
}
func main(){
person := Person{"zbw",18}
person.getName()
}
可以在方法名的前面括号中加入struct对象,表示当前方法是Person的方法
但是要注意,如果我们使用这种值传递,那么我们无法改变对象内部属性
所以这里我们想要修改需要传入指针,也就是引用传递
func (this *Person)SetName(name string){
this.Name = name
}
实际上大多数情况下定义struct的方法都是直接使用指针
权限
Go语言中没有权限修饰符
所以我们要表示公有也就是对外访问,需要将类名或者属性名大写
比如上面的Person和Name
如果用小写,那只能在本包中使用
方法也是如此
继承
type Student struct{
Person
Score int
}
func (this *Student)GetName(score int){
fmt.println("学生姓名为:"this.Name)
}
func (this *Student)setScore(score int){
this.Score = score
}
和其他语言之中同样,可以重写父类方法和定义自己的方法
student := Student{Person{"zbw",18},100}
定义子类对象的时候也是用父类对象初始化这个父类属性
这样可以调用父类方法或者子类重写的或者独有的方法
var Student s
s.Name = "zbw"
s.Age = 18
s.Score = 100
当然直接声明然后再设置也可以
多态
Go语言中的接口是通过接口interface实现的
Go语言中的接口是我在学习过程中感觉最具特色的一部分
interface本质上是一个指针
type Person interface{
Sleep()
Eat()
Drink() string
}
type Student struct{
Name string
}
func (this *Student)Eat(){
fmt.println("eat")
}
func (this *Student)Sleep(){
fmt.println("sleep")
}
func (this *Student)Drink() string{
fmt.println("drink")
return "drink"
}
定义接口和定义结构体的方式一样
接口中要定义一系列方法,按照方法名-参数列表-返回值类型的形式定义
Go语言中只要实现了接口中的所有方法就认为是实现了这个接口
这也不难理解,接口本身就是为了规范子类的行为
既然实现了全部方法何尝不是实现了这个接口呢?
当然如果有任意一个接口的方法没有被实现就不具备这种关系
var p Person // 声明接口数据类型,实际上就是父类指针
p = &Student{"zbw"}
p.Sleep()
而Go语言中的多态,可以让父类指针指向子类对象,如果是Java转来的同学要注意这里需要使用取址符
func Eating (p Person){
p.Eat()
}
那么就相当于我们可以将参数定义为接口
然后根据传入不同的子类对象调用不同子类对象中的Eat方法
在后面简单实现rpc的文章中会使用到这一特性
s = Student{"zbw"}
Eating(&s)
这样就可以通过子类对象传入用接口参数接收
让父类指针指向子类对象调用子类行为实现多态
同样要注意这里传入的是地址,因为interface本质上是指针
空接口—万能类型与断言
在Go语言中我们可以通过interface{}表示万能类型
func method(arg interface{}){
fmt.println(arg)
}
Go语言中基本数据类型都实现了interface{}
这样就可以使用interface{}类型的数据,可以引用任意的数据类型
这样我们可以传入任何类型的对象作为参数
那如何区分引用的数据类型实际上是什么呢?
Golang中给interface{}提供了一种类型断言的机制
可以通变量.(数据类型)
的方式判断
func method(arg interface{}){
value,ok := arg.(string)
if !ok{
fmt.println("当前传入参数类型不合法,请重新传入参数")
}else{
fmt.println(value)
}
}
断言传回两个参数,一个是该数据类型的变量和一个布尔类型的变量
返回值是true表示该变量是对应的数据类型
false则相反
那么断言是如何实现的呢?
这是因为在Go语言中一个变量包含两个部分(成为一个pair)
- type
- value
很容易理解就是类型和具体的数据值
而type又可以分为static type和concrete type两种类型
static type就是常见的int、string等
而concrete type就是指的是interface所指向的数据类型
这两种类型是在两个当中选择一种
通过反射机制可以通过变量找到当前变量的信息
当我们声明一个变量之后,type就不会改变了
无论我们如何给变量赋值或者用这个变量给别的变量赋值,都只是改变了对应的value
其中type都会被继承过去不会改变
断言就是相当于将这个pair找出来如果变量类型一致就将value返回
注意这里定义了两个接口Writer和Reader
然后让Book实现这两个接口
那么r.(Writer)这个断言为什么可以成功呢?
这是因为w和r具体的type是一致的