Takip et

Go Dili: Kalıtımsız Bir Yaklaşım ve Başarısının Sırrı

Go Dili: Kalıtımsız Bir Yaklaşım ve Başarısının Sırrı Modern yazılım geliştirme dünyasında, Go dili basitliği, performansı ve eşzamanlılık yetenekleriyle öne çıkıyor.

Go Dili: Kalıtımsız Bir Yaklaşım ve Başarısının Sırrı

Modern yazılım geliştirme dünyasında, Go dili basitliği, performansı ve eşzamanlılık yetenekleriyle öne çıkıyor. Ancak Go’nun en dikkat çekici özelliklerinden biri, geleneksel nesne yönelimli programlama (OOP) dillerinin temel taşı olan kalıtımı (inheritance) reddetmesidir. Peki, Go geliştiricileri bu köklü kavramdan neden vazgeçti? Bu makalede, Go’nun kalıtım yerine tercih ettiği kompozisyon ve arayüz (interface) tabanlı yaklaşımın ardındaki felsefeyi, avantajlarını ve gerçek dünya senaryolarındaki uygulamalarını derinlemesine inceleyeceğiz. Bu sayede, Go’nun yazılım tasarımına getirdiği yenilikçi bakış açısını adım adım keşfedeceksiniz.

Go’nun Kalıtıma Karşı Durmasının Ardındaki Felsefe Ne?

Yazılım geliştirme tarihinde kalıtım, kodun yeniden kullanılabilirliğini ve hiyerarşik yapıları kolaylaştırmak için güçlü bir araç olarak kabul edilmiştir. Bir sınıfın (üst sınıf) özelliklerini ve davranışlarını başka bir sınıfa (alt sınıf) devretme mekanizması olan kalıtım, “bir …’dır” (IS-A) ilişkisini modellemek için sıkça kullanılmıştır. Örneğin, bir Araba bir Taşıt‘tır veya bir Öğrenci bir Kişi‘dir. Bu yaklaşım, ilk bakışta mantıklı ve düzenli görünse de, özellikle büyük ve karmaşık sistemlerde zamanla bir dizi sorunu beraberinde getirmiştir. Go dilinin tasarımcıları, Rob Pike, Ken Thompson ve Robert Griesemer, bu sorunları yakından deneyimlemiş ve daha basit, daha esnek ve daha sürdürülebilir bir alternatif arayışına girmişlerdir. Onlar, kalıtımın getirdiği katı bağımlılıkların, kodun bakımını zorlaştırdığını, test edilebilirliği düşürdüğünü ve yazılımın evrimleşme yeteneğini kısıtladığını fark ettiler. Bu nedenle, Go’nun temel felsefesi olan “basitlik” ve “açıklık” ilkeleri doğrultusunda, kalıtımı tamamen dışarıda bırakma kararı aldılar.

Go’nun kalıtımı reddetmesinin ardında yatan temel felsefe, yazılım bileşenleri arasındaki bağımlılıkları mümkün olduğunca azaltmak ve her bir bileşenin kendi sorumluluğunu net bir şekilde tanımlamasını sağlamaktır. Geleneksel kalıtım hiyerarşileri, genellikle alt sınıfları üst sınıfların iç detaylarına sıkıca bağlar. Bu durum, üst sınıfta yapılan en ufak bir değişikliğin, ondan türeyen tüm alt sınıfları etkilemesine neden olabilir ki buna “kırılgan temel sınıf” (fragile base class) problemi denir. Go, bu tür bağımlılıkları ortadan kaldırarak, geliştiricilere daha modüler, daha esnek ve daha kolay test edilebilir sistemler inşa etme imkanı sunar. Bunun yerine, “kompozisyon” (composition) ve “arayüzler” (interfaces) adını verdiği iki güçlü mekanizmayı ön plana çıkarır. Kompozisyon, nesnelerin diğer nesneleri içermesiyle “bir …’e sahiptir” (HAS-A) ilişkilerini modellemeyi sağlarken, arayüzler davranışsal sözleşmeler tanımlayarak farklı türlerin aynı davranış setini paylaşmasına olanak tanır. Bu iki mekanizma bir araya geldiğinde, Go’da kalıtımın sunduğu avantajların çoğunu, onun dezavantajları olmadan elde etmek mümkün hale gelir. Bu yaklaşım, özellikle eşzamanlılık ve ölçeklenebilirlik gerektiren modern sistemler için kritik bir öneme sahiptir, çünkü bağımsız bileşenlerin paralel olarak çalışması ve yönetilmesi çok daha kolaydır. Sonuç olarak, Go’nun kalıtıma karşı duruşu, sadece bir tasarım tercihi değil, aynı zamanda daha güvenilir, daha hızlı ve daha yönetilebilir yazılımlar geliştirme vizyonunun bir yansımasıdır.

Geleneksel Nesne Yönelimli Programlamada Kalıtım Nedir ve Sorunları Nelerdir?

Kalıtım, nesne yönelimli programlamanın (OOP) temel direklerinden biridir ve bir sınıfın başka bir sınıftan özellikler (alanlar) ve davranışlar (metotlar) edinmesini sağlayan bir mekanizmadır. Bu, genellikle “bir …’dır” (IS-A) ilişkisini ifade etmek için kullanılır; örneğin, Köpek bir Hayvan‘dır. Kalıtımın en büyük avantajı, kodun yeniden kullanılabilirliğini artırmasıdır. Ortak özellikler ve metotlar bir üst sınıfta tanımlanır ve alt sınıflar bunları doğrudan kullanabilir veya kendi ihtiyaçlarına göre özelleştirebilir (override). Bu sayede, aynı kodu birden fazla yerde yazmaktan kaçınılır ve geliştirme süreci hızlanır. Ayrıca, kalıtım sayesinde hiyerarşik yapılar oluşturulabilir, bu da karmaşık sistemlerin daha düzenli ve anlaşılır bir şekilde organize edilmesine yardımcı olur. Polimorfizm (çok biçimlilik) de kalıtımla yakından ilişkilidir; bir üst sınıf türündeki bir değişken, alt sınıfların nesnelerini tutabilir ve bu nesneler üzerinde üst sınıfın metotları çağrıldığında, aslında alt sınıfların özelleştirilmiş metotları çalışır.

Ancak kalıtım, sunduğu avantajların yanı sıra, özellikle büyük ve uzun ömürlü yazılım projelerinde bir dizi önemli sorunu da beraberinde getirir. Bu sorunların başında “sıkı bağlılık” (tight coupling) gelir. Alt sınıflar, üst sınıfların iç detaylarına sıkıca bağlı hale gelir. Üst sınıfta yapılan herhangi bir değişiklik, alt sınıfların davranışlarını beklenmedik şekillerde etkileyebilir ve bu da “kırılgan temel sınıf” problemine yol açar. Bir üst sınıfın davranışını değiştirmek istediğinizde, ondan türeyen tüm alt sınıfları ve hatta bu alt sınıfları kullanan kodları da gözden geçirmeniz gerekebilir. Bu durum, bakım maliyetlerini artırır ve kodun evrimleşmesini zorlaştırır. Diğer bir sorun ise “hiyerarşi karmaşıklığı”dır. Çok katmanlı kalıtım hiyerarşileri zamanla içinden çıkılmaz bir hal alabilir. Hangi metotun hangi sınıftan geldiğini, nerede override edildiğini veya hangi yan etkileri olabileceğini anlamak zorlaşır. Özellikle “elmas problemi” gibi çoklu kalıtım senaryolarında (bir sınıfın birden fazla üst sınıftan türemesi), hangi üst sınıftan hangi metotun miras alınacağı konusunda belirsizlikler ortaya çıkabilir. Birçok dil bu problemi çözmek için çoklu kalıtımı kısıtlamıştır, ancak bu da tasarım esnekliğini azaltır.

Kalıtımın bir diğer dezavantajı ise “esneklik eksikliği”dir. Bir sınıf yalnızca tek bir üst sınıftan türeyebilir (çoklu kalıtıma izin veren diller hariç). Bu, bir nesnenin birden fazla bağımsız davranışı veya özelliği birleştirmesi gerektiğinde sorun yaratır. Örneğin, hem Uçabilir hem de Yüzebilir özelliklere sahip bir nesne yaratmak istediğinizde, tek kalıtım yapısı yetersiz kalabilir. Bu tür durumlarda, karmaşık arayüz hiyerarşileri veya yardımcı sınıflar kullanmak gerekebilir ki bu da kodu daha karmaşık hale getirir. Ayrıca, kalıtım genellikle “beyaz kutu yeniden kullanımı” (white-box reuse) olarak adlandırılır, çünkü alt sınıflar üst sınıfların iç işleyişine erişebilir ve bu da kapsüllemeyi (encapsulation) zayıflatır. Alt sınıf, üst sınıfın nasıl çalıştığını bildiği için, üst sınıfın iç yapısında yapılan değişiklikler alt sınıfı doğrudan etkiler. Bu sorunlar, özellikle büyük ölçekli ve uzun ömürlü yazılım projelerinde yönetilmesi zor hale gelebilir ve Go dilinin tasarımcılarını kalıtım yerine farklı bir yaklaşım benimsemeye iten temel nedenler olmuştur. Bu alternatif yaklaşım, “kompozisyonu kalıtıma tercih et” (composition over inheritance) prensibi üzerine kuruludur ve Go’da arayüzlerle birleşerek çok daha esnek ve sürdürülebilir çözümler sunar.

Go’nun Çözümü: Kompozisyon ve Arayüzler (Interfaces) Nasıl Çalışır?

Go dilinin kalıtım yerine sunduğu temel çözümler, kompozisyon ve arayüzlerdir. Bu iki kavram bir araya gelerek, nesneler arası ilişkileri modellemede ve kodun yeniden kullanılabilirliğini sağlamada kalıtımdan çok daha esnek ve yönetilebilir bir yol sunar. Go’nun bu yaklaşımı, yazılım bileşenleri arasında gevşek bağlılığı teşvik eder ve sistemlerin daha modüler bir yapıda tasarlanmasına olanak tanır.

Kompozisyon Nedir?

Kompozisyon, bir nesnenin başka bir nesneyi içermesi (HAS-A ilişkisi) prensibine dayanır. Yani, bir nesne kendi davranışlarını veya özelliklerini başka bir nesneye delege eder. Go’da kompozisyon, genellikle struct (yapı) türlerinin içine başka struct türlerini gömmek (embedding) yoluyla gerçekleştirilir. Bu sayede, gömülü struct’ın alanları ve metotları, gömen struct tarafından doğrudan erişilebilir hale gelir, tıpkı kendi üyeleriymiş gibi. Ancak bu bir kalıtım değildir; Go derleyicisi sadece bu üyeler için bir kısayol (syntactic sugar) sağlar. Temelde, içteki struct hala ayrı bir varlıktır ve dıştaki struct onun üzerinde bir “sahip olma” ilişkisi kurar. Bu mekanizma, kalıtımın getirdiği sıkı bağlılık sorununu ortadan kaldırır çünkü gömen struct, gömülü struct’ın iç işleyişine bağımlı olmak yerine, sadece onun dış arayüzünü kullanır. Bir struct’ın içine başka bir struct’ı gömmek, kod tekrarını azaltırken, aynı zamanda bileşenlerin bağımsızlığını korur.

Bir örnekle açıklayalım: Bir Arabanın bir Motoru vardır. Geleneksel OOP’de Araba, Motordan türeyebilir veya Motoru bir alan olarak içerebilir. Go’da ise Motoru Araba struct’ının içine gömerek bu ilişkiyi çok daha zarif bir şekilde ifade edebiliriz:


package main

import "fmt"

type Motor struct {
	Güç int
	Tip string
}

func (m Motor) Çalıştır() {
	fmt.Printf("%s motor %d beygir gücüyle çalışıyor.\n", m.Tip, m.Güç)
}

type Araba struct {
	Marka string
	Model string
	Motor // Motor struct'ını Araba'ya gömüyoruz
}

func main() {
	bmwMotor := Motor{Güç: 250, Tip: "Benzinli"}
	bmw := Araba{
		Marka: "BMW",
		Model: "X5",
		Motor: bmwMotor, // Gömülü Motor alanını başlatıyoruz
	}

	fmt.Printf("Araba: %s %s\n", bmw.Marka, bmw.Model)
	bmw.Çalıştır() // Araba nesnesi üzerinden Motor'un Çalıştır metoduna erişim
}
  

Yukarıdaki örnekte, Araba struct'ı Motor struct'ını gömmüştür. Bu sayede bmw.Çalıştır() çağrısı doğrudan Motor struct'ının Çalıştır metodunu tetikler. Bu, Arabanın Motorun özelliklerini ve davranışlarını "sahip olma" ilişkisiyle kullandığı anlamına gelir, bir "kalıtım" ilişkisiyle değil. Bu yaklaşım, bileşenlerin birbirine sıkıca bağlanmasını engeller ve daha modüler bir tasarım sağlar. Eğer Motor struct'ında bir değişiklik olursa, bu değişiklik sadece Motorun kendisini ve onunla doğrudan etkileşime giren kodu etkiler, Araba struct'ının iç yapısını değil. Bu, bakım kolaylığı ve esneklik açısından büyük bir avantajdır.

Arayüzler (Interfaces) Nedir?

Go'daki arayüzler, davranışsal sözleşmeler tanımlayan soyut türlerdir. Bir arayüz, belirli metot imzalarının bir koleksiyonunu tanımlar. Bir struct (veya herhangi bir tür), bu arayüzde tanımlanan tüm metotları uyguladığında, o arayüzü "örtük olarak" (implicitly) implement etmiş olur. Bu, diğer OOP dillerindeki implements anahtar kelimesine veya açık bir bildirime gerek olmadığı anlamına gelir. Go'nun arayüzleri, "davranışsal polimorfizm" sağlamanın anahtarıdır. Yani, farklı türdeki nesneler, aynı arayüzü uyguladıkları sürece, aynı şekilde ele alınabilir ve aynı metotlar üzerinden çağrılabilirler. Bu, kodun esnekliğini ve yeniden kullanılabilirliğini büyük ölçüde artırır.

Örnek olarak, farklı şekillerin alanını hesaplamak istediğimizi düşünelim:


package main

import (
	"fmt"
	"math"
)

// Şekil arayüzü, Alan() metodunu tanımlar
type Şekil interface {
	Alan() float64
}

// Daire struct'ı
type Daire struct {
	Yarıçap float64
}

// Daire, Şekil arayüzünü Alan() metodunu uygulayarak implement eder
func (d Daire) Alan() float64 {
	return math.Pi * d.Yarıçap * d.Yarıçap
}

// Kare struct'ı
type Kare struct {
	Kenar float64
}

// Kare, Şekil arayüzünü Alan() metodunu uygulayarak implement eder
func (k Kare) Alan() float64 {
	return k.Kenar * k.Kenar
}

// Şekil arayüzünü kabul eden bir fonksiyon
func ŞekilBilgisiniYazdır(s Şekil) {
	fmt.Printf("Şeklin alanı: %.2f\n", s.Alan())
}

func main() {
	d := Daire{Yarıçap: 5}
	k := Kare{Kenar: 4}

	ŞekilBilgisiniYazdır(d) // Daire nesnesini Şekil arayüzü olarak kullanıyoruz
	ŞekilBilgisiniYazdır(k) // Kare nesnesini Şekil arayüzü olarak kullanıyoruz
}
  

Bu örnekte, Şekil bir arayüzdür ve Alan() metodunu tanımlar. Hem Daire hem de Kare struct'ları, bu Alan() metodunu kendi içlerinde uygulayarak Şekil arayüzünü otomatik olarak implement ederler. ŞekilBilgisiniYazdır fonksiyonu, parametre olarak bir Şekil arayüzü bekler. Bu sayede, hem Daire hem de Kare nesnelerini bu fonksiyona geçirebiliriz. Bu yaklaşım, kodun daha esnek olmasını sağlar; yeni bir şekil (örneğin Üçgen) eklemek istediğimizde, sadece Üçgen struct'ını ve onun Alan() metodunu yazmamız yeterli olur, mevcut kodu değiştirmemize gerek kalmaz. Bu, "açık/kapalı prensibi"nin (Open/Closed Principle) mükemmel bir örneğidir: Yazılım varlıkları geliştirmeye açık, ancak değiştirmeye kapalı olmalıdır.

Kompozisyon ve arayüzler birleştiğinde, Go'da kalıtımın sunduğu tüm avantajlar (kod tekrarı, polimorfizm) çok daha esnek ve sürdürülebilir bir şekilde elde edilir. Kompozisyonla davranışları bir araya getirir, arayüzlerle de bu davranışlar üzerinde soyutlamalar tanımlarız. Bu, Go'nun modern yazılım geliştirme paradigmasında neden bu kadar başarılı olduğunun temelini oluşturur.

Go'nun Yaklaşımının Avantajları Nelerdir?

Go dilinin kalıtımdan vazgeçerek kompozisyon ve arayüzlere odaklanması, yazılım geliştirme sürecine bir dizi önemli avantaj getirir. Bu avantajlar, Go'yu özellikle büyük ölçekli, eşzamanlı ve yüksek performanslı uygulamalar için cazip bir seçenek haline getirir.

Esneklik (Flexibility): Go'nun yaklaşımı, katı kalıtım hiyerarşilerinin getirdiği sınırlamaları ortadan kaldırır. Bir struct, istediği kadar struct'ı gömebilir ve istediği kadar arayüzü implement edebilir. Bu durum, bir nesnenin birden fazla, bağımsız davranış setini veya özelliği bir araya getirmesi gerektiğinde inanılmaz bir esneklik sağlar. Geleneksel kalıtımda, bir sınıf yalnızca tek bir üst sınıftan türeyebilir (çoklu kalıtım destekleyen diller hariç ve onların da kendi karmaşıklıkları vardır). Go'da ise, bir struct'a yeni bir özellik veya davranış eklemek için, ilgili struct'ı gömmek veya yeni bir arayüz implement etmek yeterlidir. Bu, kodun yeniden düzenlenmesini ve evrimleşmesini çok daha kolay hale getirir. Örneğin, bir Logger ve bir Authenticator özelliğini bir HTTPHandler struct'ına eklemek istediğinizde, bunları ayrı ayrı gömerek kolayca birleştirebilirsiniz, tek bir kalıtım zincirine bağlı kalmadan.

Gevşek Bağlılık (Loose Coupling): Kompozisyon ve arayüzler, yazılım bileşenleri arasında gevşek bağlılığı teşvik eder. Gömülü struct'lar, dış struct'ın iç detaylarına bağımlı değildir; sadece kendi dış arayüzlerini sunarlar. Aynı şekilde, arayüzler sadece bir davranış sözleşmesi tanımlar, bu davranışı nasıl uygulandığına dair herhangi bir bilgi içermezler. Bu, bir bileşenin iç yapısında yapılan değişikliklerin, diğer bileşenleri etkileme olasılığını minimize eder. Örneğin, bir veritabanı bağlantısı struct'ının içindeki bağlantı havuzlama mantığını değiştirdiğinizde, bu struct'ı kullanan diğer struct'ların kodunu değiştirmenize gerek kalmaz, çünkü onlar sadece Veritabanı arayüzünün metotlarını çağırırlar. Gevşek bağlılık, sistemin daha kararlı, daha az hata eğilimli ve daha kolay bakımı yapılabilir olmasını sağlar.

Modülerlik (Modularity): Gevşek bağlılığın doğal bir sonucu olarak, Go'nun yaklaşımı modülerliği artırır. Her bir bileşen (struct veya arayüz), kendi sorumluluğunu net bir şekilde tanımlar ve bağımsız olarak geliştirilebilir, test edilebilir ve dağıtılabilir. Bu, özellikle mikroservis mimarileri veya büyük, dağıtık sistemler geliştirirken çok önemlidir. Geliştiriciler, sistemin farklı parçaları üzerinde bağımsız olarak çalışabilir ve bu parçalar daha sonra kolayca bir araya getirilebilir. Modüler tasarım, kodun daha düzenli olmasını, daha kolay anlaşılmasını ve yeni özelliklerin eklenmesinin daha hızlı olmasını sağlar.

Test Edilebilirlik (Testability): Gevşek bağlılık ve modülerlik, test edilebilirliği önemli ölçüde artırır. Arayüzler sayesinde, bir bileşenin bağımlılıklarını kolayca "mock" (sahte) edebilir veya "stub" (taklit) edebiliriz. Bir fonksiyon veya struct belirli bir arayüzü beklediğinde, gerçek implementasyon yerine bu arayüzü uygulayan bir test nesnesi (mock object) sağlayabiliriz. Bu, birim testlerinin (unit tests) daha izole bir şekilde yazılmasını sağlar, dış bağımlılıkların test sonuçlarını etkilemesini engeller ve testlerin daha hızlı çalışmasına olanak tanır. Örneğin, bir HTTPHandler'ın veritabanı bağımlılığını test ederken, gerçek bir veritabanı bağlantısı kurmak yerine, Veritabanı arayüzünü uygulayan bir mock veritabanı nesnesi kullanabiliriz.

Basitlik ve Anlaşılabilirlik (Simplicity and Readability): Go'nun kalıtımsız yaklaşımı, dilin genel basitlik felsefesiyle uyumludur. Karmaşık kalıtım hiyerarşileri, genellikle kodun okunabilirliğini ve anlaşılabilirliğini zorlaştırır. Go'da ise, bir struct'ın ne yaptığını anlamak genellikle daha kolaydır, çünkü içindeki alanları ve gömülü struct'ları doğrudan görebiliriz. Arayüzler de sadece bir davranış sözleşmesi sunar, bu da kodun neleri yapabileceğini anlamayı kolaylaştırır. Bu basitlik, yeni geliştiricilerin Go'ya adapte olmasını hızlandırır ve ekip içinde kodun daha tutarlı bir şekilde yazılmasına yardımcı olur. Ayrıca, daha az "sihir" ve daha açık ilişkiler, hataların ayıklanmasını (debugging) da kolaylaştırır.

Eşzamanlılık (Concurrency) ile Uyumu: Go, goroutine'ler ve kanallar (channels) ile eşzamanlılık için tasarlanmıştır. Kalıtım hiyerarşileri, genellikle paylaşılan durum (shared state) ve kilitleme (locking) mekanizmalarıyla karmaşık hale gelebilir, bu da eşzamanlı programlamada hatalara yol açabilir. Kompozisyon ve arayüzler, daha küçük, bağımsız ve kendi durumlarını yöneten bileşenler oluşturmayı teşvik eder. Bu tür bileşenler, goroutine'ler arasında veri paylaşımını daha güvenli ve kontrollü bir şekilde yapmayı kolaylaştırır, çünkü her bileşen kendi sorumluluğunu net bir şekilde taşır ve gereksiz bağımlılıklar yoktur. Bu, Go'nun eşzamanlılık modelinin gücünü tam olarak kullanmasına yardımcı olur ve daha güvenilir, paralel uygulamalar geliştirmeyi mümkün kılar.

Bu avantajlar bir araya geldiğinde, Go'nun kalıtımı reddetme kararının sadece bir tercih değil, aynı zamanda modern yazılım geliştirmenin zorluklarına yönelik pratik ve etkili bir çözüm olduğu ortaya çıkar.

Gerçek Dünya Senaryolarında Go'nun Kompozisyon Yaklaşımı Nasıl Uygulanır?

Go'nun kompozisyon ve arayüz tabanlı yaklaşımı, gerçek dünya uygulamalarında birçok farklı senaryoda etkin bir şekilde kullanılabilir. Bu yaklaşım, sistemlerin daha esnek, modüler ve sürdürülebilir olmasını sağlar. İşte iki farklı vaka analizi ile Go'nun bu güçlü yönlerini daha yakından inceleyelim.

Vaka Analizi 1: Bir Web Sunucusu Uygulaması

Modern web uygulamalarında, HTTP isteklerini işleyen sunucular genellikle çeşitli özelliklere ihtiyaç duyar: isteğin günlüğe kaydedilmesi (logging), kullanıcı kimlik doğrulaması (authentication), yetkilendirme (authorization), hata işleme ve çeşitli iş mantıkları. Geleneksel OOP kalıtımında, bu özellikler genellikle bir BaseHandler sınıfından türetilerek veya karmaşık bir "decorator" desen hiyerarşisiyle eklenmeye çalışılır. Ancak bu durum, yukarıda bahsettiğimiz gibi sıkı bağlılık ve hiyerarşi karmaşıklığına yol açabilir. Go'da ise bu tür senaryolar, kompozisyon ve arayüzler kullanılarak çok daha zarif ve esnek bir şekilde çözülür.

Go'da, bir HTTP isteğini işleyen fonksiyonlar genellikle http.Handler arayüzünü implement ederler. Bu arayüz sadece tek bir metot tanımlar: ServeHTTP(w http.ResponseWriter, r *http.Request). Bir web sunucusu uygulamasında, logging, authentication gibi çapraz kesen endişeler (cross-cutting concerns), "middleware" (ara yazılım) desenleri kullanılarak kompozisyonla uygulanır. Middleware, bir isteği asıl işleyiciye (handler) göndermeden önce veya gönderdikten sonra belirli işlemler yapan fonksiyonlardır.

Örneğin, bir Logger ve bir Authenticator middleware'i oluşturabiliriz:


package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

// LogMiddleware, gelen istekleri günlüğe kaydeder
func LogMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r)
		log.Printf("İstek: %s %s, Süre: %v\n", r.Method, r.URL.Path, time.Since(start))
	})
}

// AuthMiddleware, istekleri kimlik doğrulamadan geçirir
func AuthMiddleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Basit bir kimlik doğrulama kontrolü
		user := r.Header.Get("X-API-KEY")
		if user == "" {
			http.Error(w, "Yetkilendirme Gerekli", http.StatusUnauthorized)
			return
		}
		// Kimlik doğrulama başarılı, isteği bir sonraki işleyiciye aktar
		next.ServeHTTP(w, r)
	})
}

// Ana iş mantığını içeren Handler
type AnaIsleyici struct{}

func (h *AnaIsleyici) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Merhaba, Go Web Sunucusu! İstek yolu: %s\n", r.URL.Path)
}

func main() {
	// Ana işleyiciyi oluştur
	anaIsleyici := &AnaIsleyici{}

	// Middleware'leri kompozisyonla birbirine sar
	// İlk önce kimlik doğrulama, sonra loglama
	finalHandler := LogMiddleware(AuthMiddleware(anaIsleyici))

	http.Handle("/", finalHandler)
	log.Println("Sunucu 8080 portunda dinliyor...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}
  

Bu örnekte, LogMiddleware ve AuthMiddleware fonksiyonları, http.Handler arayüzünü kabul eden ve yeni bir http.Handler döndüren fonksiyonlardır. Bu say

Yorumlar
İçeriği beğendiniz mi? Bir tartışma başlatın veya görüşlerinizi paylaşın.
Yorum Yaz

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir

Gönder

E-posta Bülteni
Yazılım Topluluğuna Katılın
En son güncellemeleri, yaratıcı ipuçlarını ve özel kaynakları doğrudan e-posta kutunuza alın. Tasarım ve inovasyonun geleceğini birlikte keşfedelim.
Exit mobile version