Go Fonksiyonları
Go'da fonksiyonlar birinci sınıf vatandaşlardır, fonksiyonlar Go'nun en temel bileşenidir ve aynı zamanda Go'nun çekirdeğidir.
Tanımlama
Fonksiyon tanımlama formatı şöyledir:
func FonksiyonAdı([parametre listesi]) [dönüş değeri] {
fonksiyon gövdesi
}Fonksiyon tanımlamanın iki yolu vardır, biri func anahtar kelimesi ile doğrudan tanımlama, diğeri var anahtar kelimesi ile tanımlama, aşağıda gösterildiği gibi:
func sum(a int, b int) int {
return a + b
}
var sum = func(a int, b int) int {
return a + b
}Fonksiyon imzası fonksiyon adı, parametre listesi ve dönüş değerinden oluşur. Aşağıda tam bir örnek verilmiştir, fonksiyon adı Sum, iki int türü parametre a, b, dönüş değeri türü int.
func Sum(a int, b int) int {
return a + b
}Çok önemli bir nokta da Go'daki fonksiyonların overload'ı desteklememesidir, aşağıdaki kod derlenemez:
type Person struct {
Name string
Age int
Address string
Salary float64
}
func NewPerson(name string, age int, address string, salary float64) *Person {
return &Person{Name: name, Age: age, Address: address, Salary: salary}
}
func NewPerson(name string) *Person {
return &Person{Name: name}
}Go'nun felsefesi şudur: Eğer imza farklıysa, bu iki tamamen farklı fonksiyondur, o zaman aynı adı almamalıdır. Fonksiyon overload'ı kodu karışık ve anlaşılması zor hale getirebilir. Bu felsefenin doğru olup olmadığı tartışılabilir, en azından Go'da sadece fonksiyon adından ne işe yaradığını bilebilirsiniz, hangi overload olduğunu bulmanıza gerek yoktur.
Parametreler
Go'da parametre adı belirtilmeyebilir, genellikle bu arayüz veya fonksiyon türü tanımlamalarında kullanılır, ancak okunabilirlik için genellikle parametrelere ad eklenmesi önerilir:
type ExWriter func(io.Writer) error
type Writer interface {
ExWrite([]byte) (int, error)
}Aynı türdeki parametreler için türü sadece bir kez tanımlamak yeterlidir, ancak koşulu yan yana olmalarıdır:
func Log(format string, a1, a2 any) {
...
}Değişken uzunlukta parametreler 0 veya daha fazla değeri kabul edebilir, parametre listesinin sonunda tanımlanmalıdır. En tipik örnek fmt.Printf fonksiyonudur.
func Printf(format string, a ...any) (n int, err error) {
return Fprintf(os.Stdout, format, a...)
}Bahsetmeye değer bir nokta, Go'daki fonksiyon parametreleri değer ile aktarımdır, yani parametre aktarılırken gerçek parametrenin değeri kopyalanır. Slice veya map aktarırken çok fazla bellek kopyalanacağını düşünüyorsanız, endişelenmenize gerek yok çünkü bu iki veri yapısı doğası gereği işaretçidir.
Dönüş Değerleri
Aşağıda basit bir fonksiyon dönüş değeri örneği verilmiştir, Sum fonksiyonu bir int türü değer döndürür.
func Sum(a, b int) int {
return a + b
}Fonksiyonun dönüş değeri olmadığında, void gerekmez, dönüş değeri olmadan bırakılabilir.
func ErrPrintf(format string, a ...any) {
_, _ = fmt.Fprintf(os.Stderr, format, a...)
}Go fonksiyonların birden fazla dönüş değerine sahip olmasına izin verir, bu durumda dönüş değerlerini parantez içine almanız gerekir.
func Div(a, b float64) (float64, error) {
if a == 0 {
return math.NaN(), errors.New("0 bölen olarak kullanılamaz")
}
return a / b, nil
}Go ayrıca isimli dönüş değerlerini de destekler, parametre adlarıyla aynı olamaz. İsimli dönüş değerleri kullanıldığında, return anahtar kelimesi hangi değerlerin döndürüleceğini belirtmek zorunda değildir.
func Sum(a, b int) (ans int) {
ans = a + b
return
}Parametreler gibi, aynı türde birden fazla isimli dönüş değeri olduğunda, tekrarlayan tür tanımlamaları atlanabilir:
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
return
}İsimli dönüş değerleri nasıl tanımlanırsa tanımlansın, her zaman return anahtar kelimesinden sonraki değerler en yüksek önceliğe sahiptir.
func SumAndMul(a, b int) (c, d int) {
c = a + b
d = a * b
// c, d döndürülmeyecek
return a + b, a * b
}Anonim Fonksiyonlar
Anonim fonksiyon imzası olmayan fonksiyondur, örneğin aşağıdaki func(a, b int) int fonksiyonu, adı yoktur, bu yüzden sadece fonksiyon gövdesinden sonra parantez ekleyerek çağırabiliriz.
func main() {
func(a, b int) int {
return a + b
}(1, 2)
}Bir fonksiyonu çağırırken, parametresi bir fonksiyon türü olduğunda, artık ad önemli değildir, doğrudan anonim bir fonksiyon aktarabilirsiniz, aşağıda gösterildiği gibi:
type Person struct {
Name string
Age int
Salary float64
}
func main() {
people := []Person{
{Name: "Alice", Age: 25, Salary: 5000.0},
{Name: "Bob", Age: 30, Salary: 6000.0},
{Name: "Charlie", Age: 28, Salary: 5500.0},
}
slices.SortFunc(people, func(p1 Person, p2 Person) int {
if p1.Name > p2.Name {
return 1
} else if p1.Name < p2.Name {
return -1
}
return 0
})
}Bu özel sıralama kuralı örneğidir, slices.SortFunc iki parametre kabul eder, biri slice, diğeri karşılaştırma fonksiyonudur. Yeniden kullanımı düşünmezsek, doğrudan anonim fonksiyon aktarabiliriz.
Closure
Closure (kapama) kavramı, bazı dillerde Lambda ifadesi olarak da adlandırılır, anonim fonksiyonlarla birlikte kullanılır. Closure = fonksiyon + ortam referansı mı? Aşağıdaki örneğe bakalım:
func main() {
grow := Exp(2)
for i := range 10 {
fmt.Printf("2^%d=%d\n", i, grow())
}
}
func Exp(n int) func() int {
e := 1
return func() int {
temp := e
e *= n
return temp
}
}Çıktı:
2^0=1
2^1=2
2^2=4
2^3=8
2^4=16
2^5=32
2^6=64
2^7=128
2^8=256
2^9=512Exp fonksiyonunun dönüş değeri bir fonksiyondur, burada grow fonksiyonu olarak adlandırılacaktır. Her çağrıldığında, e değişkeni üstel olarak artar. grow fonksiyonu Exp fonksiyonunun iki değişkenine referans verir: e ve n. Bunlar Exp fonksiyonunun kapsamında oluşturulur ve normal durumlarda Exp fonksiyonu çağrısı bittikten sonra, bu değişkenlerin belleği yığından çıkarken geri alınır. Ancak grow fonksiyonu onlara referans verdiği için, geri alınamazlar ve yığın üzerine kaçarlar. Exp fonksiyonunun yaşam döngüsü sona ermiş olsa bile, e ve n değişkenlerinin yaşam döngüsü sona ermez. grow fonksiyonu içinde bu iki değişken doğrudan değiştirilebilir, grow fonksiyonu bir closure fonksiyonudur.
Closure kullanarak Fibonacci dizisini hesaplayan bir fonksiyonu çok basit bir şekilde uygulayabiliriz, kod aşağıdaki gibidir:
func main() {
// 10 Fibonacci sayısı
fib := Fib(10)
for n, next := fib(); next; n, next = fib() {
fmt.Println(n)
}
}
func Fib(n int) func() (int, bool) {
a, b, c := 1, 1, 2
i := 0
return func() (int, bool) {
if i >= n {
return 0, false
} else if i < 2 {
f := i
i++
return f, true
}
a, b = b, c
c = a + b
i++
return a, true
}
}Çıktı:
0
1
1
2
3
5
8
13
21
34Gecikmeli Çağrı
defer anahtar kelimesi bir fonksiyonun bir süre gecikmeli olarak çağrılmasını sağlar. Fonksiyon dönmeden önce bu defer ile tanımlanan fonksiyonlar sırayla çalıştırılır. Aşağıdaki örneğe bakalım:
func main() {
Do()
}
func Do() {
defer func() {
fmt.Println("1")
}()
fmt.Println("2")
}Çıktı:
2
1defer fonksiyon dönmeden önce çalıştırıldığı için, defer içinde fonksiyonun dönüş değerini de değiştirebilirsiniz:
func main() {
fmt.Println(sum(3, 5))
}
func sum(a, b int) (s int) {
defer func() {
s -= 10
}()
s = a + b
return
}Birden fazla defer ile tanımlanan fonksiyon olduğunda, yığın gibi son giren ilk çıkar (LIFO) sırasıyla çalıştırılır.
func main() {
fmt.Println(0)
Do()
}
func Do() {
defer fmt.Println(1)
fmt.Println(2)
defer fmt.Println(3)
defer fmt.Println(4)
fmt.Println(5)
}0
2
5
4
3
1Gecikmeli çağrı genellikle dosya kaynaklarını serbest bırakmak, ağ bağlantılarını kapatmak vb. işlemler için kullanılır. Bir diğer kullanım da panic yakalamaktır, ancak bu hata işleme bölümünde ele alınacaktır.
Döngü
Açıkça yasaklanmamış olsa da, genellikle for döngüsünde defer kullanılması önerilmez, aşağıda gösterildiği gibi:
func main() {
n := 5
for i := range n {
defer fmt.Println(i)
}
}Çıktı:
4
3
2
1
0Bu kodun sonucu doğrudur, ancak süreç belki de doğru değildir. Go'da, her bir defer oluşturulduğunda, mevcut goroutine için bir bellek alanı talep edilmesi gerekir. Yukarıdaki örnekte basit bir for n döngüsü değil de, daha karmaşık bir veri işleme akışı olduğunu varsayalım. Dış istek sayısı aniden arttığında, kısa sürede çok sayıda defer oluşturulacaktır. Döngü sayısı çok büyük veya belirsiz olduğunda, bellek kullanımının aniden artmasına neden olabilir, buna genellikle bellek sızıntısı denir.
Parametre Ön Hesaplama
Gecikmeli çağrı için bazı sezgisel olmayan detaylar vardır, örneğin aşağıdaki örnek:
func main() {
defer fmt.Println(Fn1())
fmt.Println("3")
}
func Fn1() int {
fmt.Println("2")
return 1
}Bu tuzak oldukça gizlidir, yazar daha önce bu tuzak yüzünden nedenini yarım gün boyunca bulamamıştı. Çıktının ne olacağını tahmin edebilirsiniz, cevap aşağıdaki gibidir:
2
3
1Birçok kişi aşağıdaki gibi bir çıktı olduğunu düşünebilir:
3
2
1Kullanıcının amacı doğrultusunda, fmt.Println(Fn1()) kısmının fonksiyon gövdesi çalıştıktan sonra çalışması beklenir. fmt.Println gerçekten son çalıştırılır, ancak Fn1() beklenmedik bir durumdur. Aşağıdaki örnek daha belirgindir.
func main() {
var a, b int
a = 1
b = 2
defer fmt.Println(sum(a, b))
a = 3
b = 4
}
func sum(a, b int) int {
return a + b
}Çıktı kesinlikle 3'tür, 7 değil. Eğer closure kullanılsaydı ve gecikmeli çağrı kullanılmasaydı, sonuç farklı olurdu:
func main() {
var a, b int
a = 1
b = 2
f := func() {
fmt.Println(sum(a, b))
}
a = 3
b = 4
f()
}Closure çıktısı 7'dir. Peki gecikmeli çağrı ve closure birleştirilirse:
func main() {
var a, b int
a = 1
b = 2
defer func() {
fmt.Println(sum(a, b))
}()
a = 3
b = 4
}Bu sefer normal, 7 çıktısı alınır. Şimdi tekrar değiştirelim, closure yok:
func main() {
var a, b int
a = 1
b = 2
defer func(num int) {
fmt.Println(num)
}(sum(a, b))
a = 3
b = 4
}Çıktı tekrar 3'e döner. Yukarıdaki birkaç örneği karşılaştırarak şu kodun:
defer fmt.Println(sum(a,b))Aslında şuna eşdeğer olduğu görülebilir:
defer fmt.Println(3)Go, sum fonksiyonunu en son çağırmayı beklemez, sum fonksiyonu gecikmeli çağrı çalıştırılmadan önce çağrılır ve fmt.Println'e parametre olarak aktarılır. Özetle, defer'in doğrudan etkilediği fonksiyon için, parametreleri ön hesaplanır. Bu da ilk örnekteki garip fenomene neden olur. Bu duruma, özellikle gecikmeli çağrıda fonksiyon dönüş değerini parametre olarak kullanma durumunda dikkat edilmelidir.
