三十分钟让你入门Go语言(Java小子版)

2023年 9月 12日 54.3k 0

前言

Go语言定义

Go(又称 Golang)是 Google 的 Robert Griesemer,Rob Pike 及 Ken Thompson 开发的一种静态、强类型、编译型语言。Go 语言语法与 C 相近,但功能上有:内存安全,GC,结构形态及 CSP-style 并发计算

适用范围

本篇文章适用于学习过其他面向对象语言(Java、Php),但没有学过Go语言的初学者。文章主要从Go与Java功能上的对比来阐述Go语言的基础语法、面向对象编程、并发与错误四个方面。

一、基础语法

Go语言的基础语法与常规的编程语言基本类似,所不同的有声明变量的方式,数组、切片、字典的概念及功能与Java不太相同,不过Java中这些数据结构都可以通过类比功能的方式在Go中使用。

1.1 变量、常量、nil与零值、方法、包、可见性、指针

1.1.1 变量声明

Go语言中有两种方式

1.使用var关键字声明,且需要注意的是,与大多数强类型语言不同,Go语言的声明变量类型位于变量名称的后面。Go语句结束不需要分号。

var num int

var result string = "this is result"

2.使用:=赋值。

num := 3 等同于 var num int = 3

其中变量的类型会根据右侧的值进行匹配,例如"3"会匹配为int,"3.0"会匹配为float64,"result"会匹配为string。

1.1.2 常量声明

使用const来声明一个常量,一个常量在声明后不可改变。

const laugh string = "go"

1.1.3 nil与零值

只声明未赋值的变量,其值为nil。类似于java中的“null”。

没有明确初始值的变量声明会被赋予它们的 零值。

零值是:

  • 数值类型为 0
  • 布尔类型为 false
  • 字符串为 ""(空字符串)。

1.1.4 方法、包

Go中方法的定义

使用func关键字来定义一个方法,后面跟方法名,然后是参数,返回值(如果有的话,没有返回值则不写)。

func MethodName(p1 Parm, p2 Parm) int{}

//学习一个语言应该从Hello World开始!
package main

import "fmt"

func main() {
	fmt.Println("Hello World!")// Hello World!
    fmt.Println(add(3, 5)) //8
    var sum = add(3, 5)
}

func add(a int, b int) int{
    return a+b;
}
多个返回值

Go 函数与其他编程语言一大不同之处在于支持多返回值,这在处理程序出错的时候非常有用。例如,如果上述 add 函数只支持非负整数相加,传入负数则会报错。

//返回值只定义了类型 没有定义返回参数
func add(a, b int) (int, error) {
    if a < 0 || b < 0 {
        err := errors.New("只支持非负整数相加")
        return 0, err
    }
    a *= 2
    b *= 3
    return a + b, nil
}

//返回值还定义了参数 这样可以直接return 并且定义的参数可以直接使用 return时只会返回这两个参数
func add1(a, b int) (z int, err error) {
    if a < 0 || b < 0 {
        err := errors.New("只支持非负整数相加")
        return   //实际返回0 err 因为z只定义没有赋值 则nil值为0
    }
    a *= 2
    b *= 3
    z = a + b
    return //返回 z err
}

func main()  {
    x, y := -1, 2
    z, err := add(x, y)
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    fmt.Printf("add(%d, %d) = %d\n", x, y, z)
}
变长参数
func myfunc(numbers ...int) {
    for _, number := range numbers {
        fmt.Println(number)
    }
}

slice := []int{1, 2, 3, 4, 5}
//使用...将slice打碎传入
myfunc(slice...)
包与可见性

在 Go 语言中,无论是变量、函数还是类属性和成员方法,它们的可见性都是以包为维度的,而不是类似传统面向编程那样,类属性和成员方法的可见性封装在所属的类中,然后通过 privateprotectedpublic 这些关键字来修饰其可见性。

Go 语言没有提供这些关键字,不管是变量、函数,还是自定义类的属性和成员方法,它们的可见性都是根据其首字母的大小写来决定的,如果变量名、属性名、函数名或方法名首字母大写,就可以在包外直接访问这些变量、属性、函数和方法,否则只能在包内访问,因此 Go 语言类属性和成员方法的可见性都是包一级的,而不是类一级的。

假如说一个名为domain的文件夹下有3个.go文件,则三个文件中的package都应为domain,其中程序的入口main方法所在的文件,包为main

//定义了此文件属于 main 包
package main

//通过import导入标注库中包
import "fmt"

func main() {
	fmt.Println("Hello World!")// Hello World!
    fmt.Println(add(3, 5)) //8
    var sum = add(3, 5)
}

func add(a int, b int) int{
    return a+b;
}

1.1.5 指针

对于学过C语言来说,指针还是比较熟悉的,我所理解的指针,其实就是一个在内存中实际的16进制的地址值,引用变量的值通过此地址去内存中取出对应的真实值。

func main() {
    i := 0
    //使用&来传入地址
    fmt.Println(&i) //0xc00000c054
    
    var a, b int = 3 ,4
    //传入 0xc00000a089 0xc00000a090
    fmt.Println(add(&a, &b)) 
}

//使用*来声明一个指针类型的参数与使用指针
func add(a *int, b *int)int{
    //接收到 0xc00000a089 0xc00000a090
    //前往 0xc00000a089位置查找具体数据 并取赋给x
    x := *a
    //前往 0xc00000a090位置查找具体数据 并取赋给y
    y := *b
	return x+y
}

1.2 条件、循环、分支

1.2.1 条件

与Java语言的if基本相同

// if
if condition { 
    // do something 
}

// if...else...
if condition { 
    // do something 
} else {
    // do something 
}

// if...else if...else...
if condition1 { 
    // do something 
} else if condition2 {
    // do something else 
} else {
    // catch-all or default 
}

1.2.2 循环

sum := 0 

//普通for循环
for i := 1; i vars as $name => $value) {
            $template = str_replace('{' . $name . '}', $value, $template);
        }

        return $template;
    }
}

这个时候,如果有另外有一个接口 iTemplate2 声明了与 iTemplate 完全一样的接口方法,甚至名字也叫 iTemplate,只不过位于不同的命名空间下,编译器也会认为上面的类 Template 只实现了 iTemplate 而没有实现 iTemplate2 接口。

这在我们之前的认知中是理所当然的,无论是类与类之间的继承,还是类与接口之间的实现,在 Java、PHP 这种单继承语言中,存在着严格的层级关系,一个类只能直接继承自一个父类,一个类也只能实现指定的接口,如果没有显式声明继承自某个父类或者实现某个接口,那么这个类就与该父类或者该接口没有任何关系。

我们把这种接口称为侵入式接口,所谓「侵入式」指的是实现类必须明确声明自己实现了某个接口。这种实现方式虽然足够明确和简单明了,但也存在一些问题,尤其是在设计标准库的时候,因为标准库必然涉及到接口设计,接口的需求方是业务实现类,只有具体编写业务实现类的时候才知道需要定义哪些方法,而在此之前,标准库的接口就已经设计好了,我们要么按照约定好的接口进行实现,如果没有合适的接口需要自己去设计,这里的问题就是接口的设计和业务的实现是分离的,接口的设计者并不能总是预判到业务方要实现哪些功能,这就造成了设计与实现的脱节。

接口的过分设计会导致某些声明的方法实现类完全不需要,如果设计的太简单又会导致无法满足业务的需求,这确实是一个问题,而且脱离了用户使用场景讨论这些并没有意义,以 PHP 自带的 SessionHandlerInterface 接口为例,该接口声明的接口方法如下:

SessionHandlerInterface {
    /* 方法 */
    abstract public close ( void ) : bool
    abstract public destroy ( string $session_id ) : bool
    abstract public gc ( int $maxlifetime ) : int
    abstract public open ( string $save_path , string $session_name ) : bool
    abstract public read ( string $session_id ) : string
    abstract public write ( string $session_id , string $session_data ) : bool
}

用户自定义的 Session 管理器需要实现该接口,也就是要实现该接口声明的所有方法,但是实际在做业务开发的时候,某些方法其实并不需要实现,比如如果我们基于 Redis 或 Memcached 作为 Session 存储器的话,它们自身就包含了过期回收机制,所以 gc 方法根本不需要实现,又比如 close 方法对于大部分驱动来说,也是没有什么意义的。

正是因为这种不合理的设计,所以在编写 PHP 类库中的每个接口时都需要纠结以下两个问题(Java 也类似):

  • 一个接口需要声明哪些接口方法?
  • 如果多个类实现了相同的接口方法,应该如何设计接口?比如上面这个 SessionHandlerInterface,有没有必要拆分成多个更细分的接口,以适应不同实现类的需要?
  • 接下我们来看看 Go 语言的接口是如何避免这些问题的。

    2.2.2 Go 语言的接口实现

    在 Go 语言中,类对接口的实现和子类对父类的继承一样,并没有提供类似 implement 这种关键字显式声明该类实现了哪个接口,一个类只要实现了某个接口要求的所有方法,我们就说这个类实现了该接口。

    例如,我们定义了一个 File 类,并实现了 Read()Write()Seek()Close() 四个方法:

    type File struct { 
        // ...
    }
    
    func (f *File) Read(buf []byte) (n int, err error) 
    func (f *File) Write(buf []byte) (n int, err error) 
    func (f *File) Seek(off int64, whence int) (pos int64, err error) 
    func (f *File) Close() error
    

    假设我们有如下接口(Go 语言通过关键字 interface 来声明接口,以示和结构体类型的区别,花括号内包含的是待实现的方法集合):

    type IFile interface { 
        Read(buf []byte) (n int, err error) 
        Write(buf []byte) (n int, err error) 
        Seek(off int64, whence int) (pos int64, err error) 
        Close() error 
    }
    
    type IReader interface { 
        Read(buf []byte) (n int, err error) 
    }
    
    type IWriter interface { 
        Write(buf []byte) (n int, err error) 
    }
    
    type ICloser interface { 
        Close() error 
    }
    

    尽管 File 类并没有显式实现这些接口,甚至根本不知道这些接口的存在,但是我们说 File 类实现了这些接口,因为 File 类实现了上述所有接口声明的方法。当一个类的成员方法集合包含了某个接口声明的所有方法,换句话说,如果一个接口的方法集合是某个类成员方法集合的子集,我们就认为该类实现了这个接口。

    与 Java、PHP 相对,我们把 Go 语言的这种接口称作非侵入式接口,因为类与接口的实现关系不是通过显式声明,而是系统根据两者的方法集合进行判断。这样做有两个好处:

    • 其一,Go 语言的标准库不需要绘制类库的继承/实现树图,在 Go 语言中,类的继承树并无意义,你只需要知道这个类实现了哪些方法,每个方法是干什么的就足够了。
    • 其二,定义接口的时候,只需要关心自己应该提供哪些方法即可,不用再纠结接口需要拆得多细才合理,也不需要为了实现某个接口而引入接口所在的包,接口由使用方按需定义,不用事先设计,也不用考虑之前是否有其他模块定义过类似接口。

    这样一来,就完美地避免了传统面向对象编程中的接口设计问题。

    三、并发与多线程

    3.1 Goroutine

    对于任何一个优秀的语言来说,并发处理的能力都是决定其优劣的关键。在Go语言中,通过Goroutine来实现并发的处理。

    func say(s string) {
    	fmt.Println(s)
    }
    
    func main() {
        //通过 go 关键字新开一个协程
    	go say("world")
    	say("hello")
    }
    

    Go语言中没有像Java那么多的锁来限制资源同时访问,只提供了Mutex来进行同步操作。

    //给类SafeCounter添加锁
    type SafeCounter struct {
    	v   map[string]int
    	mux sync.Mutex
    }
    
    // Inc 增加给定 key 的计数器的值。
    func (c *SafeCounter) Inc(key string) {
        //给该对象上锁
    	c.mux.Lock()
    	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
    	c.v[key]++
        //解锁
    	c.mux.Unlock()
    }
    

    3.2 Channel

    多协程之间通过Channel进行通信,从功能上可以类比为Java的volatile关键字。

    ch := make(chan int) 声明一个int型的Channel,两个协程之间可以通过ch进行int数据通信。

    通过Channel进行数据传输。

    ch

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论