关于php:三十分钟入门基础GoJava小子版

52次阅读

共计 11715 个字符,预计需要花费 30 分钟才能阅读完成。

作者:京东科技 韩国凯

前言

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 语言中,无论是变量、函数还是类属性和成员办法,它们的可见性都是以包为维度的,而不是相似传统面向编程那样,类属性和成员办法的可见性封装在所属的类中,而后通过 privateprotected 和 public 这些关键字来润饰其可见性。

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 <= 100; i++ {sum += i}

// 有限循环
for{
    sum++
    if sum = 100{break;}
}

// 带条件的循环
for res := sum+1; sum < 15{
    sum++
    res++
}

// 应用 kv 循环一个 map 或一个数组  k 为索引或键值 v 为值 k、v 不须要时能够用_带替
for k, v := range a {fmt.Println(k, v)
}

1.2.3 分支

score := 100
switch score {
case 90, 100:
    fmt.Println("Grade: A")
case 80:
    fmt.Println("Grade: B")
case 70:
    fmt.Println("Grade: C")
case 65:
    fmt.Println("Grade: D")
default:
    fmt.Println("Grade: F")
}

1.3 数组、切片、字典

1.3.1 数组

数组性能与 Java 语言相似,都是长度不可变,并且能够应用多维数组,也能够通过 arrays[i]来存储或获取值。

// 申明
var nums [3]int 
// 申明并初始化
var nums = [3]int{1,2,3} <==> nums:=[3]int{1,2,3}

// 应用
for sum := 0, i := 0;i<10{sum += nums[i]
    i++
}
// 批改值
num[0] = -1

数组应用较为简单,然而存在着难以解决的问题:长度固定

例如当咱们在程序中须要一个数据结构来存储获取到的所有用户,因为用户数量是会随着工夫变动的,然而数组其长度却不可扭转,所以数组并不适宜存储长度会产生扭转的数据。因而在 Go 语言中通过应用切片来解决以上问题。

1.3.2 切片

切片相比于 Java 来说是一种全新的概念。在 Java 中,对于不定长的数据存储构造,能够应用 List 接口来实现操作,例如有 ArrayList 与 LinkList,这些接口能够实现数据的随时增加与获取,并没有对长度进行限度。然而在 Go 中不存在这样的接口,而是 通过切片 (Slice) 来实现不定长的数据长度存储

切片与数组最大的不同就是切片不必申明长度。然而切片与数组并非毫无关系,数组能够看作是切片的底层数组,而切片则能够看作是数组某个间断片段的援用。切片能够只应用数组的一部分元素或者整个数组来创立,甚至能够创立一个比所基于的数组还要大的切片:

长度、容量

切片的长度就是它所蕴含的元素个数。

切片的容量是从它的第一个元素开始数,到其底层数组元素开端的个数。

切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。

切片的长度从性能上类比与 Java 中 List 的 size(),即通过 len(slice)来感知切片的长度,即可对 len(slice)进行循环,来动态控制切片内的具体内容。切片的容量在理论开发中使用不多,理解其概念即可。

创立切片
// 申明一个数组
var nums =[3]int{1, 2, 3}
//0. 间接申明
var slice =[]int{0, 1, 2}

//1. 从数组中援用切片 其中 a:b 是指包含 a 但不包含 b
var slice1 = nums[0:2] //{1,2}
// 如果不写的则默认为 0(右边)或最大值(左边)var slice2 = slice1[:2] <==> var slice2 = slice1[0:] <==>var slice2 = slice1[:]

//2. 应用 make 创立 Slice 其中 int 为切片类型,4 为其长度,5 为容量
slice3 := make([]int, 5)
slice4 := make([]int, 4, 5)
动静操作切片
// 应用 append 向切片中动静的增加元素
func append(s []T, vs ...T) []T

slice5 := make([]int, 4, 5) //{0, 0, 0, 0}
slice5 = append(slice5, 1) //{0,0,0,0,1}

// 删除第一个 0
sliece5 = slice5[1:]
切片的罕用场景

模仿上述提到的问题应用切片解决方案

// 申明切片
var userIds = []int{}
// 模仿获取所有用户 ID
for i := 0; i< 100{userIds = append(userIdS, i);
    i++;
}
// 对用户信息进行解决
for k,v := range userIds{userIds[k] = v++
}

1.3.3 字典

字典也可称为‘键值对’或‘key-value’,是一种罕用的数据结构,Java 中有各种 Map 接口,罕用的有 HashMap 等。在 Go 中通过应用字典来实现键值对的存储,字典是无序的,所以不会依据增加程序来保证数据的程序。

字典的申明与初始化
//string 为键类型,int 为值类型
maps := map[string]int{
  "java" : 1,
  "go" : 2,
  "python" : 3,
}

// 还能够通过 make 来创立字典 100 为其初始容量 超出可扩容
maps = make(map[string]int, 100)
字典的应用场景
// 间接应用
fmt.Println(maps["java"]) //1

// 赋值
maps["go"] = 4

// 取值 同时判断 map 中是否存在该键 ok 为 bool 型
value, ok := maps["one"] 
if ok { // 找到了
  // 解决找到的 value 
}

// 删除
delete(testMap, "four")

二、面向对象编程

2.1 Go 语言中的类

家喻户晓,在面向对象的语言中,一个类应该具备属性、构造方法、成员办法三种构造,Go 语言也不例外。

2.1.1 类的申明与初始化

Go 语言中并没有明确的类的概念,只有 struct 关键字能够从性能上类比为 面向对象语言中的“类”。比方要定义一个学生类,能够这么做:

type Student struct {
    id int
    name string
    male bool
    score float64
}// 定义了一个学生类,属性有 id name 等,每个属性的类型都在其前面

// 定义学生类的构造方法
func NewStudent(id uint, name string, male bool, score float64) *Student {return &Student{id, name, male, score}
}

// 实例化一个类对象
student := NewStudent(1, "学院君", 100)
fmt.Println(student)

2.1.2 成员办法

Go 中的成员办法申明与其余语言不大雷同。以 Student 类为例,

// 在办法名前,增加对应的类,即可认为改办法为该类的成员办法。func (s Student) GetName() string  {return s.name}

// 留神这里的 Student 是带了 * 的 这是因为在办法传值过程中 存在着值传递与援用传递 即指针的概念 当应用值传递时 编译器会为该参数创立一个正本传入 因而如果对正本进行批改其实是不失效的 因为在执行完此办法后该正本会被销毁 所以此处应该是用 *Student 将要批改的对象指针传入 批改值能力起作用
func (s *Student) SetName(name string) {// 这里其实是应该应用(*s).name = name,因为对于一个地址来说 其属性是没意义的 不过这样应用也是能够的 因为编译器会帮咱们主动转换
    s.name = name
}

2.2 接口

接口在 Go 语言中有着至关重要的位置,如果说 goroutine 和 channel 是撑持起 Go 语言并发模型的基石,那么接口就是 Go 语言整个类型零碎的基石。Go 语言的接口不单单只是接口,上面就让咱们一步步来摸索 Go 语言的接口个性。

2.2.1 传统侵入式接口实现

和类的实现类似,Go 语言的接口和其余语言中提供的接口概念齐全不同。以 Java、PHP 为例,接口次要作为不同类之间的契约(Contract)存在,对契约的实现是强制的,体现在具体的细节上就是如果一个类实现了某个接口,就必须实现该接口申明的所有办法,这个叫「履行契约」:

// 申明一个 'iTemplate' 接口
interface iTemplate
{public function setVariable($name, $var);
    public function getHtml($template);
}


// 实现接口
// 上面的写法是正确的
class Template implements iTemplate
{private $vars = array();

    public function setVariable($name, $var)
    {$this->vars[$name] = $var;
    }

    public function getHtml($template)
    {foreach($this->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 也相似):

  1. 一个接口须要申明哪些接口办法?
  2. 如果多个类实现了雷同的接口办法,应该如何设计接口?比方下面这个 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 <- v    // 将 v 发送至信道 ch。v := <-ch  // 从 ch 接管值并赋予 v。
package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {sum += v}
    c <- sum // 将和送入 c
}

// 对于 main 办法来说 相当于就是开启了一个协程
func main() {s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    // 通过 go 关键字开启两个协程 将 chaneel 当做参数传入
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    // 通过箭头方向获取或传入信息
    x, y := <-c, <-c // 从 c 中接管

    fmt.Println(x, y, x+y)
}

四、错误处理

4.1 error

Go 语言错误处理机制十分简单明了,不须要学习理解简单的概念、函数和类型,Go 语言为错误处理定义了一个规范模式,即 error 接口,该接口的定义非常简单:

type error interface {Error() string 
}

其中只申明了一个 Error() 办法,用于返回字符串类型的谬误音讯。对于大多数函数或类办法,如果要返回谬误,根本都能够定义成如下模式 —— 将谬误类型作为第二个参数返回:

func Foo(param int) (n int, err error) {// ...}

而后在调用返回错误信息的函数 / 办法时,依照如下「卫述语句」模板编写解决代码即可:

n, err := Foo(0)

if err != nil {// 错误处理} else{// 应用返回值 n}

十分简洁优雅。

4.2 defer

defer 用于确保一个办法执行实现之后,无论执行后果是否胜利,都要执行 defer 中的语句。相似于 Java 中的 try..catch..finally 用法。例如在文件解决中,无论后果是否胜利,都要敞开文件流。

func ReadFile(filename string) ([]byte, error) {f, err := os.Open(filename)
    if err != nil {return nil, err}
    // 无论后果如何 都要敞开文件流
    defer f.Close()

    var n int64 = bytes.MinRead

    if fi, err := f.Stat(); err == nil {if size := fi.Size() + bytes.MinRead; size > n {n = size}
    }
    return readAll(f, n)
}

4.3 panic

Go 语言中没有太多的异样类,不像 Java 一样有 Error、Exception 等谬误类型,当然也没有 try..catch 语句。

Panic(恐慌),象征在程序运行中呈现了谬误,如果该谬误未被捕捉的话,就会造成零碎解体退出。例如一个简略的 panic:a := 1/0

就会引发 panic: integer divide by zero。

其中第一行示意出问题的协程,第二行是问题代码所在的包和函数,第三行是问题代码的具体位置,最初一行则是程序的退出状态,通过这些信息,能够帮忙你疾速定位问题并予以解决。

4.4 recover

当有能够预感的谬误时,又不心愿程序解体退出,能够应用 recover()语句来捕捉未解决的 panic。recover 该当放在 defer 语句中,且该语句应该在办法中前部,防止未能执行到 defer 语句时就引发了零碎异样退出。

package main

import ("fmt")

func divide() {
    // 通过 defer,确保该办法只有执行结束都要执行该匿名办法
    defer func() {
        // 进行异样捕捉
        if err := recover(); err != nil {fmt.Printf("Runtime panic caught: %v\n", err)
        }
    }()

    var i = 1
    var j = 0
    k := i / j
    fmt.Printf("%d / %d = %d\n", i, j, k)
}

func main() {divide()
    fmt.Println("divide 办法调用结束,回到 main 函数")
}

能够看到,尽管会出现异常,但咱们应用 recover()捕捉之后,就不会呈现零碎解体退出的情景,而只是将该办法完结。其中 fmt.Printf("%d / %d = %d\n", i, j, k) 语句并没有执行到,因为代码执行到他的上一步曾经出现异常导致该办法提前结束。
4 recover

当有能够预感的谬误时,又不心愿程序解体退出,能够应用 recover()语句来捕捉未解决的 panic。recover 该当放在 defer 语句中,且该语句应该在办法中前部,防止未能执行到 defer 语句时就引发了零碎异样退出。

package main

import ("fmt")

func divide() {
    // 通过 defer,确保该办法只有执行结束都要执行该匿名办法
    defer func() {
        // 进行异样捕捉
        if err := recover(); err != nil {fmt.Printf("Runtime panic caught: %v\n", err)
        }
    }()

    var i = 1
    var j = 0
    k := i / j
    fmt.Printf("%d / %d = %d\n", i, j, k)
}

func main() {divide()
    fmt.Println("divide 办法调用结束,回到 main 函数")
}

能够看到,尽管会出现异常,但咱们应用 recover()捕捉之后,就不会呈现零碎解体退出的情景,而只是将该办法完结。其中 fmt.Printf("%d / %d = %d\n", i, j, k) 语句并没有执行到,因为代码执行到他的上一步曾经出现异常导致该办法提前结束。

五、总结

通过以上的学习,大家能够以应用为目标的初步理解到 go 的根底语法,然而仅凭本文想要学明确 go 是齐全不够的。例如 go 的最大劣势之一“协程”,因为文章目标就没有特地具体开展,有趣味的同学能够持续学习。

正文完
 0